Compare commits
1 Commits
4b57a1d2a6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b518c729f6 |
1296
Cargo.lock
generated
1296
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -19,10 +19,12 @@ gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
|||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr", }
|
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-memory = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-gossip-memory = { 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" }
|
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44" ] }
|
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44" ] }
|
||||||
|
|
||||||
@@ -40,10 +42,13 @@ serde_json = "1.0"
|
|||||||
schemars = "1"
|
schemars = "1"
|
||||||
smallvec = "1.14.0"
|
smallvec = "1.14.0"
|
||||||
smol = "2"
|
smol = "2"
|
||||||
tracing = "0.1.40"
|
|
||||||
webbrowser = "1.0.4"
|
webbrowser = "1.0.4"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
# Use stacker's psm version which may have better WASM support
|
||||||
|
psm = { git = "https://github.com/rust-lang/stacker", branch = "master" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ publish.workspace = true
|
|||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_tokio.workspace = true
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
@@ -18,4 +16,6 @@ serde_json.workspace = true
|
|||||||
|
|
||||||
semver = "1.0.27"
|
semver = "1.0.27"
|
||||||
tempfile = "3.23.0"
|
tempfile = "3.23.0"
|
||||||
futures.workspace = true
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|||||||
@@ -17,11 +17,10 @@ nostr-sdk.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
futures.workspace = true
|
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
|
|
||||||
fuzzy-matcher = "0.3.7"
|
fuzzy-matcher = "0.3.7"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use std::cmp::Reverse;
|
|||||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use std::sync::RwLock;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -15,8 +17,9 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use smol::lock::RwLock;
|
use smol::lock::RwLock;
|
||||||
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP};
|
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP, UniversalSigner};
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
mod room;
|
mod room;
|
||||||
@@ -44,16 +47,14 @@ pub enum ChatEvent {
|
|||||||
/// No Inbox Relays found, the app is not ready to subscribe messages
|
/// No Inbox Relays found, the app is not ready to subscribe messages
|
||||||
InboxRelayNotFound,
|
InboxRelayNotFound,
|
||||||
/// An error occurred
|
/// An error occurred
|
||||||
Error(SharedString),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Channel signal.
|
/// Channel signal.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
enum Signal {
|
enum Signal {
|
||||||
/// Inbox Relays found, the app is ready to subscribe messages
|
/// Inbox Relays found, the app is ready to subscribe messages
|
||||||
InboxReady(Box<Event>),
|
InboxReady,
|
||||||
/// No Inbox Relays found, the app is not ready to subscribe messages
|
|
||||||
InboxRelayNotFound,
|
|
||||||
/// Message received from relay pool
|
/// Message received from relay pool
|
||||||
Message(NewMessage),
|
Message(NewMessage),
|
||||||
/// Eose received from relay pool
|
/// Eose received from relay pool
|
||||||
@@ -67,18 +68,6 @@ impl Signal {
|
|||||||
Self::Message(NewMessage::new(gift_wrap, rumor))
|
Self::Message(NewMessage::new(gift_wrap, rumor))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inbox_ready(event: &Event) -> Self {
|
|
||||||
Self::InboxReady(Box::new(event.to_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn inbox_relay_not_found() -> Self {
|
|
||||||
Self::InboxRelayNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eose() -> Self {
|
|
||||||
Self::Eose
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error<T>(event: &Event, reason: T) -> Self
|
pub fn error<T>(event: &Event, reason: T) -> Self
|
||||||
where
|
where
|
||||||
T: Into<SharedString>,
|
T: Into<SharedString>,
|
||||||
@@ -105,9 +94,6 @@ pub struct ChatRegistry {
|
|||||||
/// Tracking the status of unwrapping gift wrap events.
|
/// Tracking the status of unwrapping gift wrap events.
|
||||||
tracking: Arc<AtomicBool>,
|
tracking: Arc<AtomicBool>,
|
||||||
|
|
||||||
/// Whether the messaging relays have been found.
|
|
||||||
msg_relays_existed: Arc<AtomicBool>,
|
|
||||||
|
|
||||||
/// Channel for sending signals to the UI.
|
/// Channel for sending signals to the UI.
|
||||||
signal_tx: flume::Sender<Signal>,
|
signal_tx: flume::Sender<Signal>,
|
||||||
|
|
||||||
@@ -137,17 +123,15 @@ impl ChatRegistry {
|
|||||||
/// Create a new chat registry instance
|
/// Create a new chat registry instance
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let user_signer = nostr.read(cx).signer.clone();
|
|
||||||
|
|
||||||
let (tx, rx) = flume::unbounded::<Signal>();
|
let (tx, rx) = flume::unbounded::<Signal>();
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe to the signer event
|
// Subscribe to the signer event
|
||||||
cx.observe(&user_signer, |this, signer, cx| {
|
cx.subscribe(&nostr, |this, _nostr, event, cx| {
|
||||||
if let Some(keys) = signer.read(cx).clone() {
|
if event.signer_changed() {
|
||||||
this.reset(cx);
|
this.reset(cx);
|
||||||
this.handle_notifications(keys, cx);
|
this.handle_notifications(cx);
|
||||||
this.get_metadata(cx);
|
this.get_metadata(cx);
|
||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
};
|
};
|
||||||
@@ -166,7 +150,6 @@ impl ChatRegistry {
|
|||||||
seens: Arc::new(RwLock::new(HashMap::default())),
|
seens: Arc::new(RwLock::new(HashMap::default())),
|
||||||
event_map: Arc::new(RwLock::new(HashMap::default())),
|
event_map: Arc::new(RwLock::new(HashMap::default())),
|
||||||
tracking: Arc::new(AtomicBool::new(false)),
|
tracking: Arc::new(AtomicBool::new(false)),
|
||||||
msg_relays_existed: Arc::new(AtomicBool::new(false)),
|
|
||||||
signal_rx: rx,
|
signal_rx: rx,
|
||||||
signal_tx: tx,
|
signal_tx: tx,
|
||||||
tasks: smallvec![],
|
tasks: smallvec![],
|
||||||
@@ -175,13 +158,12 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle nostr notifications
|
/// Handle nostr notifications
|
||||||
fn handle_notifications(&mut self, signer: Keys, cx: &mut Context<Self>) {
|
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
let tracking = self.tracking.clone();
|
let tracking = self.tracking.clone();
|
||||||
let msg_relays_existed = self.msg_relays_existed.clone();
|
|
||||||
|
|
||||||
let seens = self.seens.clone();
|
let seens = self.seens.clone();
|
||||||
let event_map = self.event_map.clone();
|
let event_map = self.event_map.clone();
|
||||||
let trashes = self.trashes.downgrade();
|
let trashes = self.trashes.downgrade();
|
||||||
@@ -206,12 +188,6 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
match *message {
|
match *message {
|
||||||
RelayMessage::Event { event, .. } => {
|
RelayMessage::Event { event, .. } => {
|
||||||
// Keep track of which relays have seen this event
|
|
||||||
{
|
|
||||||
let mut seens = seens.write().await;
|
|
||||||
seens.entry(event.id).or_default().insert(relay_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// De-duplicate events by their ID
|
// De-duplicate events by their ID
|
||||||
if !processed_events.insert(event.id) {
|
if !processed_events.insert(event.id) {
|
||||||
continue;
|
continue;
|
||||||
@@ -220,12 +196,9 @@ impl ChatRegistry {
|
|||||||
// Handle msg relays event to determine when the app is ready to subscribe
|
// Handle msg relays event to determine when the app is ready to subscribe
|
||||||
if event.kind == Kind::InboxRelays {
|
if event.kind == Kind::InboxRelays {
|
||||||
let current_user = signer.get_public_key_async().await?;
|
let current_user = signer.get_public_key_async().await?;
|
||||||
|
|
||||||
if event.pubkey == current_user {
|
|
||||||
// Mark that the msg relays have been found
|
|
||||||
msg_relays_existed.store(true, Ordering::Release);
|
|
||||||
// Emit the inbox ready signal
|
// Emit the inbox ready signal
|
||||||
tx.send_async(Signal::inbox_ready(&event)).await?;
|
if event.pubkey == current_user {
|
||||||
|
tx.send_async(Signal::InboxReady).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +207,12 @@ impl ChatRegistry {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep track of which relays have seen this event
|
||||||
|
{
|
||||||
|
let mut seens = seens.write().await;
|
||||||
|
seens.entry(event.id).or_default().insert(relay_url);
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the rumor from the gift wrap event
|
// Extract the rumor from the gift wrap event
|
||||||
match extract_rumor(&client, &signer, event.as_ref()).await {
|
match extract_rumor(&client, &signer, event.as_ref()).await {
|
||||||
Ok(rumor) => {
|
Ok(rumor) => {
|
||||||
@@ -268,7 +247,7 @@ impl ChatRegistry {
|
|||||||
RelayMessage::EndOfStoredEvents(id)
|
RelayMessage::EndOfStoredEvents(id)
|
||||||
if (id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2) =>
|
if (id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2) =>
|
||||||
{
|
{
|
||||||
tx.send_async(Signal::eose()).await?;
|
tx.send_async(Signal::Eose).await?;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -285,14 +264,9 @@ impl ChatRegistry {
|
|||||||
this.new_message(message, cx);
|
this.new_message(message, cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Signal::InboxReady(event) => {
|
Signal::InboxReady => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.get_messages(&event, cx);
|
this.get_messages(cx);
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Signal::InboxRelayNotFound => {
|
|
||||||
this.update(cx, |_this, cx| {
|
|
||||||
cx.emit(ChatEvent::InboxRelayNotFound);
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Signal::Eose => {
|
Signal::Eose => {
|
||||||
@@ -318,9 +292,8 @@ impl ChatRegistry {
|
|||||||
let status = self.tracking.clone();
|
let status = self.tracking.clone();
|
||||||
let tx = self.signal_tx.clone();
|
let tx = self.signal_tx.clone();
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
self.tasks.push(cx.spawn(async move |_, cx| {
|
||||||
let loop_duration = Duration::from_secs(15);
|
let loop_duration = Duration::from_secs(15);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if status.load(Ordering::Acquire) {
|
if status.load(Ordering::Acquire) {
|
||||||
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
|
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
|
||||||
@@ -328,7 +301,7 @@ impl ChatRegistry {
|
|||||||
} else {
|
} else {
|
||||||
_ = tx.send_async(Signal::Eose).await;
|
_ = tx.send_async(Signal::Eose).await;
|
||||||
}
|
}
|
||||||
smol::Timer::after(loop_duration).await;
|
cx.background_executor().timer(loop_duration).await;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -338,7 +311,7 @@ impl ChatRegistry {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -366,20 +339,34 @@ impl ChatRegistry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let tx = self.signal_tx.clone();
|
let client = nostr.read(cx).client();
|
||||||
let msg_relays_existed = self.msg_relays_existed.clone();
|
|
||||||
|
|
||||||
// Reset the status flag
|
// Spawn a task to verify user inbox relays after 5 seconds
|
||||||
msg_relays_existed.store(false, Ordering::Release);
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(5)).await;
|
||||||
|
|
||||||
// Wait for the msg relays to be found or timeout
|
if !cx
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
.background_spawn(async move {
|
||||||
// Wait for 5 seconds
|
// Construct inbox relays filter
|
||||||
smol::Timer::after(Duration::from_secs(5)).await;
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
// Then check if the msg relays have been found
|
// Check the latest inbox relays event in database
|
||||||
if !msg_relays_existed.load(Ordering::Acquire) {
|
client
|
||||||
tx.send_async(Signal::inbox_relay_not_found()).await?;
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.first_owned()
|
||||||
|
.is_some()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(ChatEvent::InboxRelayNotFound);
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -387,37 +374,47 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get all messages for the provided signer
|
/// Get all messages for the provided signer
|
||||||
fn get_messages(&mut self, msg_relays: &Event, cx: &mut Context<Self>) {
|
fn get_messages(&mut self, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let urls: Vec<RelayUrl> = nip17::extract_relay_list(msg_relays).collect();
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
|
||||||
let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex()));
|
// Construct inbox relays filter
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Get the latest inbox relays event in database
|
||||||
|
let event = client
|
||||||
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await?
|
||||||
|
.first_owned()
|
||||||
|
.ok_or(anyhow::anyhow!("No inbox relays found"))?;
|
||||||
|
|
||||||
|
// Extract relay list from event
|
||||||
|
let relays: Vec<RelayUrl> = nip17::extract_relay_list(&event).collect();
|
||||||
|
|
||||||
// Ensure relay connections
|
// Ensure relay connections
|
||||||
for url in urls.iter() {
|
for url in relays.iter() {
|
||||||
client.add_relay(url).and_connect().await?;
|
client.add_relay(url).and_connect().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct gift wrap event filter
|
||||||
|
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
|
let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex()));
|
||||||
|
|
||||||
// Construct target for subscription
|
// Construct target for subscription
|
||||||
let target: HashMap<RelayUrl, Filter> = urls
|
let target: HashMap<RelayUrl, Filter> = relays
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|relay| (relay, filter.clone()))
|
.map(|relay| (relay, filter.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let output = client.subscribe(target).with_id(id).await?;
|
client.subscribe(target).with_id(id).await?;
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Successfully subscribed to gift-wrap messages on: {:?}",
|
|
||||||
output.success
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -425,7 +422,7 @@ impl ChatRegistry {
|
|||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
if let Err(e) = task.await {
|
if let Err(e) = task.await {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(ChatEvent::Error(SharedString::from(e.to_string())));
|
cx.emit(ChatEvent::Error(e.to_string()));
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -512,7 +509,7 @@ impl ChatRegistry {
|
|||||||
{
|
{
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -623,7 +620,7 @@ impl ChatRegistry {
|
|||||||
pub fn get_rooms(&mut self, cx: &mut Context<Self>) {
|
pub fn get_rooms(&mut self, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -733,7 +730,7 @@ impl ChatRegistry {
|
|||||||
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
|
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -769,7 +766,7 @@ impl ChatRegistry {
|
|||||||
/// Unwraps a gift-wrapped event and processes its contents.
|
/// Unwraps a gift-wrapped event and processes its contents.
|
||||||
async fn extract_rumor(
|
async fn extract_rumor(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
signer: &Keys,
|
signer: &UniversalSigner,
|
||||||
gift_wrap: &Event,
|
gift_wrap: &Event,
|
||||||
) -> Result<UnsignedEvent, Error> {
|
) -> Result<UnsignedEvent, Error> {
|
||||||
// Try to get cached rumor first
|
// Try to get cached rumor first
|
||||||
@@ -793,7 +790,7 @@ async fn extract_rumor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper method to try unwrapping with different signers
|
/// Helper method to try unwrapping with different signers
|
||||||
async fn try_unwrap(signer: &Keys, gift_wrap: &Event) -> Result<UnwrappedGift, Error> {
|
async fn try_unwrap(signer: &UniversalSigner, gift_wrap: &Event) -> Result<UnwrappedGift, Error> {
|
||||||
/*
|
/*
|
||||||
* // Try with the device signer first
|
* // Try with the device signer first
|
||||||
if let Some(signer) = signer.get_encryption_signer().await {
|
if let Some(signer) = signer.get_encryption_signer().await {
|
||||||
@@ -806,13 +803,16 @@ async fn try_unwrap(signer: &Keys, gift_wrap: &Event) -> Result<UnwrappedGift, E
|
|||||||
// Fallback to the user's signer
|
// Fallback to the user's signer
|
||||||
let user_signer = signer.get().await;
|
let user_signer = signer.get().await;
|
||||||
*/
|
*/
|
||||||
let unwrapped = try_unwrap_with(gift_wrap, signer).await?;
|
let unwrapped = try_unwrap_with(signer, gift_wrap).await?;
|
||||||
|
|
||||||
Ok(unwrapped)
|
Ok(unwrapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to unwrap a gift wrap event with a given signer.
|
/// Attempts to unwrap a gift wrap event with a given signer.
|
||||||
async fn try_unwrap_with(gift_wrap: &Event, signer: &Keys) -> Result<UnwrappedGift, Error> {
|
async fn try_unwrap_with(
|
||||||
|
signer: &UniversalSigner,
|
||||||
|
gift_wrap: &Event,
|
||||||
|
) -> Result<UnwrappedGift, Error> {
|
||||||
// Get the sealed event
|
// Get the sealed event
|
||||||
let seal = signer
|
let seal = signer
|
||||||
.nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content)
|
.nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use itertools::Itertools;
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Person, PersonRegistry};
|
use person::{Person, PersonRegistry};
|
||||||
use settings::{RoomConfig, SignerKind};
|
use settings::{RoomConfig, SignerKind};
|
||||||
use state::{NostrRegistry, TIMEOUT};
|
use state::{NostrRegistry, TIMEOUT, UniversalSigner};
|
||||||
|
|
||||||
use crate::NewMessage;
|
use crate::NewMessage;
|
||||||
|
|
||||||
@@ -427,7 +427,7 @@ impl Room {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
// Get current user's public key
|
// Get current user's public key
|
||||||
let sender = nostr.read(cx).signer_pubkey(cx)?;
|
let sender = nostr.read(cx).current_user()?;
|
||||||
|
|
||||||
// Get all members, excluding the sender
|
// Get all members, excluding the sender
|
||||||
let members: Vec<Person> = self
|
let members: Vec<Person> = self
|
||||||
@@ -482,13 +482,12 @@ impl Room {
|
|||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let user_signer = nostr.read(cx).signer();
|
||||||
|
let current_user = nostr.read(cx).current_user()?;
|
||||||
|
|
||||||
// Get current user's public key
|
// Get sender's profile
|
||||||
let user_signer = nostr.read(cx).signer(cx)?;
|
|
||||||
let public_key = nostr.read(cx).signer_pubkey(cx)?;
|
|
||||||
|
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let sender = persons.read(cx).get(&public_key, cx);
|
let sender = persons.read(cx).get(¤t_user, cx);
|
||||||
|
|
||||||
// Get all members (excluding sender)
|
// Get all members (excluding sender)
|
||||||
let members: Vec<Person> = self
|
let members: Vec<Person> = self
|
||||||
@@ -597,7 +596,7 @@ impl Room {
|
|||||||
// Helper function to send a gift-wrapped event
|
// Helper function to send a gift-wrapped event
|
||||||
async fn send_gift_wrap(
|
async fn send_gift_wrap(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
signer: &Keys,
|
signer: &UniversalSigner,
|
||||||
receiver: &Person,
|
receiver: &Person,
|
||||||
rumor: &UnsignedEvent,
|
rumor: &UnsignedEvent,
|
||||||
config: &SignerKind,
|
config: &SignerKind,
|
||||||
|
|||||||
@@ -14,19 +14,16 @@ chat = { path = "../chat" }
|
|||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_tokio.workspace = true
|
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
|
||||||
|
|
||||||
once_cell = "1.19.0"
|
|
||||||
regex = "1"
|
|
||||||
linkify = "0.10.0"
|
linkify = "0.10.0"
|
||||||
pulldown-cmark = "0.13.1"
|
pulldown-cmark = "0.13.1"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
pub use actions::*;
|
pub use actions::*;
|
||||||
use anyhow::{Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error};
|
||||||
@@ -18,6 +20,7 @@ use nostr_sdk::prelude::*;
|
|||||||
use person::{Person, PersonRegistry};
|
use person::{Person, PersonRegistry};
|
||||||
use settings::{AppSettings, SignerKind};
|
use settings::{AppSettings, SignerKind};
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use smol::lock::RwLock;
|
use smol::lock::RwLock;
|
||||||
use state::{NostrRegistry, upload};
|
use state::{NostrRegistry, upload};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|||||||
@@ -9,15 +9,11 @@ gpui.workspace = true
|
|||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
smallvec.workspace = true
|
|
||||||
smol.workspace = true
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
qrcode = "0.14.1"
|
qrcode = "0.14.1"
|
||||||
bech32 = "0.11.1"
|
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
|
|||||||
@@ -14,12 +14,10 @@ settings = { path = "../settings" }
|
|||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use nostr_sdk::prelude::*;
|
|||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{Announcement, CLIENT_NAME, NostrRegistry};
|
use state::{Announcement, CLIENT_NAME, NostrRegistry, UniversalSigner};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::Button;
|
use ui::button::Button;
|
||||||
@@ -66,7 +66,7 @@ pub struct DeviceRegistry {
|
|||||||
pub announcement_existed: Arc<AtomicBool>,
|
pub announcement_existed: Arc<AtomicBool>,
|
||||||
|
|
||||||
/// Signer
|
/// Signer
|
||||||
signer: Entity<Option<Keys>>,
|
signer: Entity<Option<UniversalSigner>>,
|
||||||
|
|
||||||
/// Async tasks
|
/// Async tasks
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
@@ -90,14 +90,11 @@ impl DeviceRegistry {
|
|||||||
|
|
||||||
/// Create a new device registry instance
|
/// Create a new device registry instance
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let signer = cx.new(|_| None);
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let user_signer = nostr.read(cx).signer.clone();
|
|
||||||
|
|
||||||
let settings = AppSettings::global(cx);
|
let settings = AppSettings::global(cx);
|
||||||
let is_nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx);
|
let nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx);
|
||||||
|
|
||||||
|
let signer = cx.new(|_| None);
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
@@ -111,10 +108,10 @@ impl DeviceRegistry {
|
|||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the user signer
|
// Observe the user signer
|
||||||
cx.observe(&user_signer, move |this, signer, cx| {
|
cx.subscribe(&nostr, move |this, _nostr, event, cx| {
|
||||||
if signer.read(cx).is_some() && is_nip4e_enabled {
|
if event.signer_changed() && nip4e_enabled {
|
||||||
this.get_announcement(cx);
|
this.get_announcement(cx);
|
||||||
};
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -134,7 +131,7 @@ impl DeviceRegistry {
|
|||||||
fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let current_user = nostr.read(cx).signer_pubkey(cx);
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
let announcement_existed = self.announcement_existed.clone();
|
let announcement_existed = self.announcement_existed.clone();
|
||||||
let (tx, rx) = flume::bounded::<Event>(100);
|
let (tx, rx) = flume::bounded::<Event>(100);
|
||||||
@@ -142,6 +139,7 @@ impl DeviceRegistry {
|
|||||||
self.tasks.push(cx.background_spawn(async move {
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
let mut notifications = client.notifications();
|
let mut notifications = client.notifications();
|
||||||
let mut processed_events = HashSet::new();
|
let mut processed_events = HashSet::new();
|
||||||
|
let current_user = signer.get_public_key_async().await.ok();
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
while let Some(notification) = notifications.next().await {
|
||||||
if let ClientNotification::Message { message, .. } = notification
|
if let ClientNotification::Message { message, .. } = notification
|
||||||
@@ -205,14 +203,14 @@ impl DeviceRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the signer
|
/// Get the signer
|
||||||
pub fn signer(&self, cx: &App) -> Option<Keys> {
|
pub fn signer(&self, cx: &App) -> Option<UniversalSigner> {
|
||||||
self.signer.read(cx).clone()
|
self.signer.read(cx).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the decoupled encryption key for the current user
|
/// Set the decoupled encryption key for the current user
|
||||||
fn set_signer(&mut self, new: Keys, cx: &mut Context<Self>) {
|
fn set_signer(&mut self, new_signer: Keys, cx: &mut Context<Self>) {
|
||||||
self.signer.update(cx, |this, cx| {
|
self.signer.update(cx, |this, cx| {
|
||||||
*this = Some(new);
|
*this = Some(UniversalSigner::new(new_signer));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
cx.emit(DeviceEvent::Set);
|
cx.emit(DeviceEvent::Set);
|
||||||
@@ -222,10 +220,7 @@ impl DeviceRegistry {
|
|||||||
pub fn backup(&self, path: PathBuf, cx: &App) -> Task<Result<(), Error>> {
|
pub fn backup(&self, path: PathBuf, cx: &App) -> Task<Result<(), Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return Task::ready(Err(anyhow!("Signer is required")));
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let keys = get_keys(&client, &signer).await?;
|
let keys = get_keys(&client, &signer).await?;
|
||||||
@@ -241,13 +236,11 @@ impl DeviceRegistry {
|
|||||||
pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
|
pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
let current_user = signer.get_public_key_async().await?;
|
||||||
|
|
||||||
// Construct the filter for the device announcement event
|
// Construct the filter for the device announcement event
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
@@ -265,12 +258,13 @@ impl DeviceRegistry {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
let announcement_existed = self.announcement_existed.clone();
|
let announcement_existed = self.announcement_existed.clone();
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
if !cx
|
if !cx
|
||||||
.background_spawn(async move {
|
.background_spawn(async move {
|
||||||
// Wait for 5 seconds
|
// Wait for 5 seconds
|
||||||
smol::Timer::after(Duration::from_secs(5)).await;
|
executor.timer(Duration::from_secs(5)).await;
|
||||||
|
|
||||||
// Then check if the msg relays have been found
|
// Then check if the msg relays have been found
|
||||||
if !announcement_existed.load(Ordering::Acquire) {
|
if !announcement_existed.load(Ordering::Acquire) {
|
||||||
@@ -316,14 +310,11 @@ impl DeviceRegistry {
|
|||||||
fn create_encryption(&self, keys: Keys, cx: &App) -> Task<Result<Keys, Error>> {
|
fn create_encryption(&self, keys: Keys, cx: &App) -> Task<Result<Keys, Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
let secret = keys.secret_key().to_secret_hex();
|
let secret = keys.secret_key().to_secret_hex();
|
||||||
let n = keys.public_key();
|
let n = keys.public_key();
|
||||||
|
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return Task::ready(Err(anyhow!("Signer is required")));
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
// Construct an announcement event
|
// Construct an announcement event
|
||||||
let event = EventBuilder::new(Kind::Custom(10044), "")
|
let event = EventBuilder::new(Kind::Custom(10044), "")
|
||||||
@@ -352,10 +343,7 @@ impl DeviceRegistry {
|
|||||||
fn set_encryption(&mut self, event: &Event, cx: &mut Context<Self>) {
|
fn set_encryption(&mut self, event: &Event, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let announcement = Announcement::from(event);
|
let announcement = Announcement::from(event);
|
||||||
let device_pubkey = announcement.public_key();
|
let device_pubkey = announcement.public_key();
|
||||||
@@ -391,10 +379,7 @@ impl DeviceRegistry {
|
|||||||
fn wait_for_request(&mut self, cx: &mut Context<Self>) {
|
fn wait_for_request(&mut self, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
@@ -417,10 +402,7 @@ impl DeviceRegistry {
|
|||||||
pub fn request(&mut self, cx: &mut Context<Self>) {
|
pub fn request(&mut self, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(app_keys) = get_or_init_app_keys(cx) else {
|
let Ok(app_keys) = get_or_init_app_keys(cx) else {
|
||||||
return;
|
return;
|
||||||
@@ -485,10 +467,7 @@ impl DeviceRegistry {
|
|||||||
fn wait_for_approval(&mut self, cx: &mut Context<Self>) {
|
fn wait_for_approval(&mut self, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.emit(DeviceEvent::Requesting);
|
cx.emit(DeviceEvent::Requesting);
|
||||||
|
|
||||||
@@ -553,10 +532,7 @@ impl DeviceRegistry {
|
|||||||
fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) {
|
fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get user's write relays
|
// Get user's write relays
|
||||||
let event = event.clone();
|
let event = event.clone();
|
||||||
@@ -769,7 +745,7 @@ fn get_or_init_app_keys(cx: &App) -> Result<Keys, Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt and store device keys in the local database.
|
/// Encrypt and store device keys in the local database.
|
||||||
async fn set_keys(client: &Client, signer: &Keys, secret: &str) -> Result<(), Error> {
|
async fn set_keys(client: &Client, signer: &UniversalSigner, secret: &str) -> Result<(), Error> {
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
let content = signer.nip44_encrypt_async(&public_key, secret).await?;
|
let content = signer.nip44_encrypt_async(&public_key, secret).await?;
|
||||||
|
|
||||||
@@ -786,7 +762,7 @@ async fn set_keys(client: &Client, signer: &Keys, secret: &str) -> Result<(), Er
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get device keys from the local database.
|
/// Get device keys from the local database.
|
||||||
async fn get_keys(client: &Client, signer: &Keys) -> Result<Keys, Error> {
|
async fn get_keys(client: &Client, signer: &UniversalSigner) -> Result<Keys, Error> {
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ gpui.workspace = true
|
|||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
|
|||||||
@@ -126,13 +126,13 @@ impl Person {
|
|||||||
if let Some(display_name) = self.metadata().display_name.as_ref()
|
if let Some(display_name) = self.metadata().display_name.as_ref()
|
||||||
&& !display_name.is_empty()
|
&& !display_name.is_empty()
|
||||||
{
|
{
|
||||||
return SharedString::from(display_name);
|
return SharedString::from(display_name.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = self.metadata().name.as_ref()
|
if let Some(name) = self.metadata().name.as_ref()
|
||||||
&& !name.is_empty()
|
&& !name.is_empty()
|
||||||
{
|
{
|
||||||
return SharedString::from(name);
|
return SharedString::from(name.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedString::from(shorten_pubkey(self.public_key(), 4))
|
SharedString::from(shorten_pubkey(self.public_key(), 4))
|
||||||
|
|||||||
@@ -1,372 +0,0 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
use std::cell::Cell;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::hash::Hash;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
|
||||||
use gpui::{
|
|
||||||
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
|
|
||||||
Task, Window, div, relative,
|
|
||||||
};
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use settings::{AppSettings, AuthMode};
|
|
||||||
use smallvec::{SmallVec, smallvec};
|
|
||||||
use state::NostrRegistry;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::Button;
|
|
||||||
use ui::notification::{Notification, NotificationKind};
|
|
||||||
use ui::{Disableable, WindowExtension, v_flex};
|
|
||||||
|
|
||||||
const AUTH_MESSAGE: &str =
|
|
||||||
"Approve the authentication request to allow Coop to continue sending or receiving events.";
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) {
|
|
||||||
RelayAuth::set_global(cx.new(|cx| RelayAuth::new(window, cx)), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Authentication request
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
struct AuthRequest {
|
|
||||||
url: RelayUrl,
|
|
||||||
challenge: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthRequest {
|
|
||||||
pub fn new<S>(challenge: S, url: RelayUrl) -> Self
|
|
||||||
where
|
|
||||||
S: Into<String>,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
challenge: challenge.into(),
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn url(&self) -> &RelayUrl {
|
|
||||||
&self.url
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn challenge(&self) -> &str {
|
|
||||||
&self.challenge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
enum Signal {
|
|
||||||
Auth(Arc<AuthRequest>),
|
|
||||||
Pending((EventId, RelayUrl)),
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GlobalRelayAuth(Entity<RelayAuth>);
|
|
||||||
|
|
||||||
impl Global for GlobalRelayAuth {}
|
|
||||||
|
|
||||||
// Relay authentication
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RelayAuth {
|
|
||||||
/// Pending events waiting for resend after authentication
|
|
||||||
pending_events: HashSet<(EventId, RelayUrl)>,
|
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
|
||||||
_tasks: SmallVec<[Task<()>; 2]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RelayAuth {
|
|
||||||
/// Retrieve the global relay auth state
|
|
||||||
pub fn global(cx: &App) -> Entity<Self> {
|
|
||||||
cx.global::<GlobalRelayAuth>().0.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the global relay auth instance
|
|
||||||
fn set_global(state: Entity<Self>, cx: &mut App) {
|
|
||||||
cx.set_global(GlobalRelayAuth(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new relay auth instance
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
|
||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
|
||||||
let (tx, rx) = flume::bounded::<Signal>(256);
|
|
||||||
|
|
||||||
tasks.push(cx.background_spawn(async move {
|
|
||||||
let mut notifications = client.notifications();
|
|
||||||
let mut challenges: HashSet<Cow<'_, str>> = HashSet::default();
|
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
|
||||||
if let ClientNotification::Message { relay_url, message } = notification {
|
|
||||||
match *message {
|
|
||||||
RelayMessage::Auth { challenge } => {
|
|
||||||
if challenges.insert(challenge.clone()) {
|
|
||||||
let request = Arc::new(AuthRequest::new(challenge, relay_url));
|
|
||||||
let signal = Signal::Auth(request);
|
|
||||||
|
|
||||||
tx.send_async(signal).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RelayMessage::Ok {
|
|
||||||
event_id, message, ..
|
|
||||||
} => {
|
|
||||||
let msg = MachineReadablePrefix::parse(&message);
|
|
||||||
|
|
||||||
// Handle authentication messages
|
|
||||||
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
|
||||||
let signal = Signal::Pending((event_id, relay_url));
|
|
||||||
tx.send_async(signal).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
while let Ok(signal) = rx.recv_async().await {
|
|
||||||
match signal {
|
|
||||||
Signal::Auth(req) => {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.handle_auth(&req, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
Signal::Pending((event_id, relay_url)) => {
|
|
||||||
this.update_in(cx, |this, _window, cx| {
|
|
||||||
this.insert_pending_event(event_id, relay_url, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
pending_events: HashSet::default(),
|
|
||||||
_tasks: tasks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert a pending event waiting for resend after authentication
|
|
||||||
fn insert_pending_event(&mut self, id: EventId, relay: RelayUrl, cx: &mut Context<Self>) {
|
|
||||||
self.pending_events.insert((id, relay));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all pending events for a specific relay,
|
|
||||||
fn get_pending_events(&self, relay: &RelayUrl, _cx: &App) -> Vec<EventId> {
|
|
||||||
self.pending_events
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, pending_relay)| pending_relay == relay)
|
|
||||||
.map(|(id, _relay)| id)
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all pending events for a specific relay,
|
|
||||||
fn clear_pending_events(&mut self, relay: &RelayUrl, cx: &mut Context<Self>) {
|
|
||||||
self.pending_events
|
|
||||||
.retain(|(_, pending_relay)| pending_relay != relay);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle authentication request
|
|
||||||
fn handle_auth(&mut self, req: &Arc<AuthRequest>, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let settings = AppSettings::global(cx);
|
|
||||||
let trusted_relay = settings.read(cx).trusted_relay(req.url(), cx);
|
|
||||||
let mode = AppSettings::get_auth_mode(cx);
|
|
||||||
|
|
||||||
if trusted_relay && mode == AuthMode::Auto {
|
|
||||||
// Automatically authenticate if the relay is authenticated before
|
|
||||||
self.response(req, window, cx);
|
|
||||||
} else {
|
|
||||||
// Otherwise open the auth request popup
|
|
||||||
self.ask_for_approval(req, window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send auth response and wait for confirmation
|
|
||||||
fn auth(&self, req: &Arc<AuthRequest>, cx: &App) -> Task<Result<(), Error>> {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return Task::ready(Err(anyhow!("Signer is required")));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get all pending events for the relay
|
|
||||||
let req = req.clone();
|
|
||||||
let pending_events = self.get_pending_events(req.url(), cx);
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
// Construct event
|
|
||||||
let event = EventBuilder::auth(req.challenge(), req.url().clone())
|
|
||||||
.finalize_async(&signer)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Get the event ID
|
|
||||||
let id = event.id;
|
|
||||||
|
|
||||||
// Get the relay
|
|
||||||
let relay = client.relay(req.url()).await?.context("Relay not found")?;
|
|
||||||
|
|
||||||
// Subscribe to notifications
|
|
||||||
let mut notifications = relay.notifications();
|
|
||||||
|
|
||||||
// Send the AUTH message
|
|
||||||
relay
|
|
||||||
.send_msg(ClientMessage::Auth(Cow::Borrowed(&event)))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
|
||||||
match notification {
|
|
||||||
RelayNotification::Message { message } => {
|
|
||||||
if let RelayMessage::Ok { event_id, .. } = *message {
|
|
||||||
if id != event_id {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all subscriptions
|
|
||||||
let subscriptions = relay.subscriptions().await;
|
|
||||||
|
|
||||||
// Re-subscribe to previous subscriptions
|
|
||||||
for (id, filters) in subscriptions.into_iter() {
|
|
||||||
if !filters.is_empty() {
|
|
||||||
relay.send_msg(ClientMessage::req(id, filters)).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-send pending events
|
|
||||||
for id in pending_events {
|
|
||||||
if let Some(event) = client.database().event_by_id(&id).await? {
|
|
||||||
relay.send_event(&event).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RelayNotification::AuthenticationFailed => break,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow!("Authentication failed"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Respond to an authentication request.
|
|
||||||
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
|
|
||||||
let settings = AppSettings::global(cx);
|
|
||||||
let req = req.clone();
|
|
||||||
let challenge = SharedString::from(req.challenge().to_string());
|
|
||||||
|
|
||||||
// Create a task for authentication
|
|
||||||
let task = self.auth(&req, cx);
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let result = task.await;
|
|
||||||
let url = req.url();
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
window.clear_notification_by_id::<AuthNotification>(challenge, cx);
|
|
||||||
|
|
||||||
if let Err(e) = result {
|
|
||||||
window
|
|
||||||
.push_notification(Notification::error(e.to_string()).autohide(false), cx);
|
|
||||||
} else {
|
|
||||||
// Clear pending events for the authenticated relay
|
|
||||||
this.clear_pending_events(url, cx);
|
|
||||||
|
|
||||||
let domain = url.domain().unwrap_or_default();
|
|
||||||
let msg = format!("Relay {} has been authenticated", domain);
|
|
||||||
|
|
||||||
window.push_notification(Notification::success(msg), cx);
|
|
||||||
|
|
||||||
// Save the authenticated relay to automatically authenticate future requests
|
|
||||||
settings.update(cx, |this, cx| {
|
|
||||||
this.add_trusted_relay(url, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a popup to approve the authentication request.
|
|
||||||
fn ask_for_approval(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
|
|
||||||
let notification = self.notification(req, cx);
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
window.push_notification(notification, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a notification for the authentication request.
|
|
||||||
fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification {
|
|
||||||
let req = req.clone();
|
|
||||||
let challenge = SharedString::from(req.challenge.clone());
|
|
||||||
let url = SharedString::from(req.url().to_string());
|
|
||||||
let entity = cx.entity().downgrade();
|
|
||||||
let loading = Rc::new(Cell::new(false));
|
|
||||||
|
|
||||||
Notification::new()
|
|
||||||
.type_id::<AuthNotification>(challenge)
|
|
||||||
.autohide(false)
|
|
||||||
.with_kind(NotificationKind::Info)
|
|
||||||
.title("Authentication Required")
|
|
||||||
.content(move |_this, _window, cx| {
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.line_height(relative(1.25))
|
|
||||||
.child(SharedString::from(AUTH_MESSAGE)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.py_1()
|
|
||||||
.px_1p5()
|
|
||||||
.rounded_sm()
|
|
||||||
.text_xs()
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
|
||||||
.text_color(cx.theme().text)
|
|
||||||
.child(url.clone()),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
.action(move |_this, _window, _cx| {
|
|
||||||
let view = entity.clone();
|
|
||||||
let req = req.clone();
|
|
||||||
|
|
||||||
Button::new("approve")
|
|
||||||
.label("Approve")
|
|
||||||
.loading(loading.get())
|
|
||||||
.disabled(loading.get())
|
|
||||||
.on_click({
|
|
||||||
let loading = Rc::clone(&loading);
|
|
||||||
move |_ev, window, cx| {
|
|
||||||
// Set loading state to true
|
|
||||||
loading.set(true);
|
|
||||||
// Process to approve the request
|
|
||||||
view.update(cx, |this, cx| {
|
|
||||||
this.response(&req, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AuthNotification;
|
|
||||||
@@ -10,7 +10,6 @@ common = { path = "../common" }
|
|||||||
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
@@ -18,3 +17,6 @@ serde.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
@@ -229,13 +229,14 @@ impl AppSettings {
|
|||||||
/// Load settings
|
/// Load settings
|
||||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let task: Task<Result<Settings, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Settings, Error>> = cx.background_spawn(async move {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
let path = config_dir().join(".settings");
|
let path = config_dir().join(".settings");
|
||||||
|
|
||||||
if let Ok(content) = smol::fs::read_to_string(&path).await {
|
if let Ok(content) = smol::fs::read_to_string(&path).await {
|
||||||
Ok(serde_json::from_str(&content)?)
|
return Ok(serde_json::from_str(&content)?);
|
||||||
} else {
|
|
||||||
Err(anyhow!("Not found"))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow!("Not found"))
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
@@ -254,11 +255,10 @@ impl AppSettings {
|
|||||||
/// Save settings
|
/// Save settings
|
||||||
pub fn save(&mut self, cx: &mut Context<Self>) {
|
pub fn save(&mut self, cx: &mut Context<Self>) {
|
||||||
let settings = self.inner.read(cx);
|
let settings = self.inner.read(cx);
|
||||||
|
|
||||||
if let Ok(content) = serde_json::to_string(&settings) {
|
if let Ok(content) = serde_json::to_string(&settings) {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let path = config_dir().join(".settings");
|
let path = config_dir().join(".settings");
|
||||||
// Write settings to file
|
|
||||||
smol::fs::write(&path, content).await.ok();
|
smol::fs::write(&path, content).await.ok();
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|||||||
@@ -9,22 +9,25 @@ common = { path = "../common" }
|
|||||||
|
|
||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr-lmdb.workspace = true
|
|
||||||
nostr-gossip-memory.workspace = true
|
nostr-gossip-memory.workspace = true
|
||||||
nostr-connect.workspace = true
|
|
||||||
nostr-blossom.workspace = true
|
nostr-blossom.workspace = true
|
||||||
|
nostr-connect.workspace = true
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_tokio.workspace = true
|
|
||||||
smol.workspace = true
|
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
webbrowser.workspace = true
|
webbrowser.workspace = true
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
rustls = "0.23"
|
|
||||||
petname = "2.0.2"
|
|
||||||
whoami = "1.6.1"
|
|
||||||
mime_guess = "2.0.4"
|
mime_guess = "2.0.4"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
nostr-memory.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
nostr-lmdb.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
gpui_tokio.workspace = true
|
||||||
|
rustls = "0.23"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{Error, anyhow};
|
||||||
use gpui::AsyncApp;
|
use gpui::AsyncApp;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
use nostr_blossom::prelude::*;
|
use nostr_blossom::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub async fn upload(server: Url, path: PathBuf, cx: &AsyncApp) -> Result<Url, Error> {
|
pub async fn upload(server: Url, path: PathBuf, cx: &AsyncApp) -> Result<Url, Error> {
|
||||||
let content_type = from_path(&path).first_or_octet_stream().to_string();
|
let content_type = from_path(&path).first_or_octet_stream().to_string();
|
||||||
let data = smol::fs::read(path).await?;
|
let data = smol::fs::read(path).await?;
|
||||||
@@ -25,3 +27,8 @@ pub async fn upload(server: Url, path: PathBuf, cx: &AsyncApp) -> Result<Url, Er
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("Upload error: {e}"))?
|
.map_err(|e| anyhow!("Upload error: {e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub async fn upload(_server: Url, _path: PathBuf, _cx: &AsyncApp) -> Result<Url, Error> {
|
||||||
|
Err(anyhow!("File upload not supported on web"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ pub const COOP_PUBKEY: &str = "npub1j3rz3ndl902lya6ywxvy5c983lxs8mpukqnx4pa4lt5w
|
|||||||
pub const APP_ID: &str = "su.reya.coop";
|
pub const APP_ID: &str = "su.reya.coop";
|
||||||
|
|
||||||
/// Keyring name
|
/// Keyring name
|
||||||
pub const KEYRING: &str = "Coop Safe Storage";
|
pub const MASTER_KEYRING: &str = "Coop Master Key";
|
||||||
|
pub const USER_KEYRING: &str = "Coop User Credential";
|
||||||
|
|
||||||
/// Default timeout for subscription
|
/// Default timeout for subscription
|
||||||
pub const TIMEOUT: u64 = 2;
|
pub const TIMEOUT: u64 = 2;
|
||||||
@@ -45,7 +46,7 @@ pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.
|
|||||||
|
|
||||||
/// Default bootstrap relays
|
/// Default bootstrap relays
|
||||||
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.ditto.pub",
|
||||||
"wss://relay.primal.net",
|
"wss://relay.primal.net",
|
||||||
"wss://user.kindpag.es",
|
"wss://user.kindpag.es",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,31 +3,38 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
use common::config_dir;
|
use common::config_dir;
|
||||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window};
|
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use nostr_gossip_memory::prelude::*;
|
use nostr_gossip_memory::prelude::*;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use nostr_lmdb::prelude::*;
|
use nostr_lmdb::prelude::*;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use nostr_memory::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
mod blossom;
|
mod blossom;
|
||||||
mod constants;
|
mod constants;
|
||||||
mod nip05;
|
mod nip05;
|
||||||
mod nip4e;
|
mod nip4e;
|
||||||
|
mod signer;
|
||||||
|
|
||||||
pub use blossom::*;
|
pub use blossom::*;
|
||||||
pub use constants::*;
|
pub use constants::*;
|
||||||
pub use nip4e::*;
|
pub use nip4e::*;
|
||||||
pub use nip05::*;
|
pub use nip05::*;
|
||||||
|
pub use signer::{CoopAuthUrlHandler, UniversalSigner};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) {
|
pub fn init(window: &mut Window, cx: &mut App) {
|
||||||
// rustls uses the `aws_lc_rs` provider by default
|
// rustls uses the `aws_lc_rs` provider by default
|
||||||
// This only errors if the default provider has already
|
// This only errors if the default provider has already
|
||||||
// been installed. We can ignore this `Result`.
|
// been installed. We can ignore this `Result`.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
rustls::crypto::aws_lc_rs::default_provider()
|
rustls::crypto::aws_lc_rs::default_provider()
|
||||||
.install_default()
|
.install_default()
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
// Initialize the tokio runtime
|
// Initialize the tokio runtime
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
gpui_tokio::init(cx);
|
gpui_tokio::init(cx);
|
||||||
|
|
||||||
NostrRegistry::set_global(cx.new(|cx| NostrRegistry::new(window, cx)), cx);
|
NostrRegistry::set_global(cx.new(|cx| NostrRegistry::new(window, cx)), cx);
|
||||||
@@ -40,21 +47,27 @@ impl Global for GlobalNostrRegistry {}
|
|||||||
/// Signer event.
|
/// Signer event.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum StateEvent {
|
pub enum StateEvent {
|
||||||
/// Connecting to the bootstrapping relay
|
/// The state is busy
|
||||||
Connecting,
|
Busy,
|
||||||
/// Connected to the bootstrapping relay
|
/// User has no signer
|
||||||
Connected,
|
NoSigner,
|
||||||
|
/// The signer has changed
|
||||||
|
SignerChanged,
|
||||||
/// An error occurred
|
/// An error occurred
|
||||||
Error(SharedString),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateEvent {
|
impl StateEvent {
|
||||||
pub fn error<T>(error: T) -> Self
|
pub fn error<T>(error: T) -> Self
|
||||||
where
|
where
|
||||||
T: Into<SharedString>,
|
T: Into<String>,
|
||||||
{
|
{
|
||||||
Self::Error(error.into())
|
Self::Error(error.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn signer_changed(&self) -> bool {
|
||||||
|
matches!(self, StateEvent::SignerChanged)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nostr Registry
|
/// Nostr Registry
|
||||||
@@ -63,8 +76,11 @@ pub struct NostrRegistry {
|
|||||||
/// Nostr client
|
/// Nostr client
|
||||||
client: Client,
|
client: Client,
|
||||||
|
|
||||||
/// Currently active signer
|
/// Universal signer
|
||||||
pub signer: Entity<Option<Keys>>,
|
signer: UniversalSigner,
|
||||||
|
|
||||||
|
/// Current user's public key
|
||||||
|
current_user: Option<PublicKey>,
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
@@ -85,18 +101,24 @@ impl NostrRegistry {
|
|||||||
|
|
||||||
/// Create a new nostr instance
|
/// Create a new nostr instance
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let signer = cx.new(|_| None);
|
let signer = UniversalSigner::new(Keys::generate());
|
||||||
|
let authenticator = SignerAuthenticator::new(signer.clone());
|
||||||
|
|
||||||
// Construct the nostr lmdb instance
|
// Construct the nostr lmdb instance
|
||||||
let lmdb = cx.foreground_executor().block_on(async move {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let database = cx.foreground_executor().block_on(async move {
|
||||||
NostrLmdb::open(config_dir().join("nostr"))
|
NostrLmdb::open(config_dir().join("nostr"))
|
||||||
.await
|
.await
|
||||||
.expect("Failed to initialize database")
|
.expect("Failed to initialize database")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let database = MemoryDatabase::unbounded();
|
||||||
|
|
||||||
// Construct the nostr client
|
// Construct the nostr client
|
||||||
let client = ClientBuilder::default()
|
let client = ClientBuilder::default()
|
||||||
.database(lmdb)
|
.database(database)
|
||||||
|
.authenticator(authenticator)
|
||||||
.gossip(NostrGossipMemory::unbounded())
|
.gossip(NostrGossipMemory::unbounded())
|
||||||
.gossip_config(GossipConfig::default().no_background_refresh())
|
.gossip_config(GossipConfig::default().no_background_refresh())
|
||||||
.connect_timeout(Duration::from_secs(10))
|
.connect_timeout(Duration::from_secs(10))
|
||||||
@@ -105,14 +127,16 @@ impl NostrRegistry {
|
|||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Run at the end of current cycle
|
// Connect to bootstrap relays after the window is ready
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
this.connect(cx);
|
this.connect_bootstrap_relays(cx);
|
||||||
|
this.get_user_credential(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
client,
|
client,
|
||||||
signer,
|
signer,
|
||||||
|
current_user: None,
|
||||||
tasks: vec![],
|
tasks: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,26 +146,48 @@ impl NostrRegistry {
|
|||||||
self.client.clone()
|
self.client.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the signer
|
/// Get the current signer
|
||||||
pub fn signer(&self, cx: &App) -> Option<Keys> {
|
pub fn signer(&self) -> UniversalSigner {
|
||||||
self.signer.read(cx).clone()
|
self.signer.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the public key of the signer
|
/// Get the current user's public key
|
||||||
pub fn signer_pubkey(&self, cx: &App) -> Option<PublicKey> {
|
pub fn current_user(&self) -> Option<PublicKey> {
|
||||||
self.signer.read(cx).as_ref().map(|s| s.public_key())
|
self.current_user
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the signer to the given keys
|
/// Update the signer
|
||||||
pub fn set_signer(&mut self, new_keys: Keys, cx: &mut Context<Self>) {
|
pub fn set_signer<T>(&mut self, new_signer: T, cx: &mut Context<Self>)
|
||||||
self.signer.update(cx, |this, cx| {
|
where
|
||||||
*this = Some(new_keys);
|
T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + 'static,
|
||||||
|
<T as AsyncGetPublicKey>::Error: std::error::Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncSignEvent>::Error: std::error::Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncNip44>::Error: std::error::Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
match new_signer.get_public_key_async().await {
|
||||||
|
Ok(public_key) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.signer.swap_inner(new_signer);
|
||||||
|
this.current_user = Some(public_key);
|
||||||
|
cx.emit(StateEvent::SignerChanged);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
})?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to the bootstrapping relays
|
/// Connect to the bootstrapping relays
|
||||||
fn connect(&mut self, cx: &mut Context<Self>) {
|
fn connect_bootstrap_relays(&mut self, cx: &mut Context<Self>) {
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
@@ -164,24 +210,90 @@ impl NostrRegistry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit connecting event
|
|
||||||
cx.emit(StateEvent::Connecting);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
if let Err(e) = task.await {
|
if let Err(e) = task.await {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
})?;
|
})?;
|
||||||
} else {
|
}
|
||||||
this.update(cx, |_this, cx| {
|
Ok(())
|
||||||
cx.emit(StateEvent::Connected);
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the user's credential and set the signer if valid
|
||||||
|
fn get_user_credential(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let user_keyring = cx.read_credentials(USER_KEYRING);
|
||||||
|
let master_keyring = self.get_master_key(cx);
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
match user_keyring.await {
|
||||||
|
Ok(Some((_username, secret))) => {
|
||||||
|
let content = String::from_utf8(secret)?;
|
||||||
|
|
||||||
|
if content.starts_with("nsec1") {
|
||||||
|
let secret_key = SecretKey::parse(&content)?;
|
||||||
|
let keys = Keys::new(secret_key);
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_signer(keys, cx);
|
||||||
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
|
} else if content.starts_with("bunker://") {
|
||||||
|
let keys = master_keyring.await;
|
||||||
|
let timeout = Duration::from_secs(30);
|
||||||
|
let uri = NostrConnectUri::parse(content)?;
|
||||||
|
|
||||||
|
// Construct the nostr connect signer
|
||||||
|
let mut signer = NostrConnect::new(uri, keys, timeout, None)?;
|
||||||
|
|
||||||
|
// Handle auth url with the default browser
|
||||||
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_signer(signer, cx);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
this.update(cx, |_, cx| {
|
||||||
|
cx.emit(StateEvent::NoSigner);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the master key that used for Nostr Connect
|
||||||
|
pub fn get_master_key(&self, cx: &App) -> Task<Keys> {
|
||||||
|
let task = cx.read_credentials(MASTER_KEYRING);
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let (keys, new_key) = match task.await {
|
||||||
|
Ok(Some((_user, secret))) => match SecretKey::from_slice(&secret) {
|
||||||
|
Ok(secret_key) => (Keys::new(secret_key), false),
|
||||||
|
_ => (Keys::generate(), true),
|
||||||
|
},
|
||||||
|
_ => (Keys::generate(), true),
|
||||||
|
};
|
||||||
|
|
||||||
|
if new_key {
|
||||||
|
let keys_clone = keys.clone();
|
||||||
|
let username = keys_clone.public_key().to_hex();
|
||||||
|
let password = keys_clone.secret_key().to_secret_bytes();
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
let task = cx.write_credentials(MASTER_KEYRING, &username, &password);
|
||||||
|
cx.background_spawn(async move { task.await.ok() }).detach();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
keys
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the public key of a NIP-05 address
|
/// Get the public key of a NIP-05 address
|
||||||
pub fn query_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
|
pub fn query_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
@@ -291,10 +403,7 @@ impl NostrRegistry {
|
|||||||
pub fn wot_search(&self, query: &str, cx: &App) -> Task<Result<Vec<PublicKey>, Error>> {
|
pub fn wot_search(&self, query: &str, cx: &App) -> Task<Result<Vec<PublicKey>, Error>> {
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
let query = query.to_string();
|
let query = query.to_string();
|
||||||
|
let signer = self.signer.clone();
|
||||||
let Some(signer) = self.signer.read(cx).clone() else {
|
|
||||||
return Task::ready(Err(anyhow!("Signer is required")));
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
// Construct a vertex request event
|
// Construct a vertex request event
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use futures::io::AsyncReadExt;
|
||||||
use gpui::http_client::{AsyncBody, HttpClient};
|
use gpui::http_client::{AsyncBody, HttpClient};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smol::io::AsyncReadExt;
|
|
||||||
|
|
||||||
#[allow(async_fn_in_trait)]
|
#[allow(async_fn_in_trait)]
|
||||||
pub trait NostrAddress {
|
pub trait NostrAddress {
|
||||||
|
|||||||
196
crates/state/src/signer.rs
Normal file
196
crates/state/src/signer.rs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use nostr_connect::client::AuthUrlHandler;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UniversalSignerError(Box<dyn Error + Send + Sync + 'static>);
|
||||||
|
|
||||||
|
impl fmt::Display for UniversalSignerError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for UniversalSignerError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
Some(&*self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniversalSignerError {
|
||||||
|
pub fn new<E>(err: E) -> Self
|
||||||
|
where
|
||||||
|
E: Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
UniversalSignerError(Box::new(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct UniversalSigner {
|
||||||
|
inner: Arc<RwLock<Arc<dyn InnerSigner>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniversalSigner {
|
||||||
|
pub fn new<T>(signer: T) -> Self
|
||||||
|
where
|
||||||
|
T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + 'static,
|
||||||
|
<T as AsyncGetPublicKey>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncSignEvent>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncNip44>::Error: Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(RwLock::new(Arc::new(InnerSignerImpl(signer)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swap the inner signer in-place. All clones see the new signer.
|
||||||
|
pub fn swap_inner<T>(&self, new_signer: T)
|
||||||
|
where
|
||||||
|
T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + 'static,
|
||||||
|
<T as AsyncGetPublicKey>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncSignEvent>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncNip44>::Error: Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
*self.inner.write().expect("RwLock poisoned") = Arc::new(InnerSignerImpl(new_signer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait InnerSigner: fmt::Debug + Send + Sync + 'static {
|
||||||
|
fn get_public_key_async(&self) -> BoxedFuture<'_, Result<PublicKey, UniversalSignerError>>;
|
||||||
|
fn sign_event_async(
|
||||||
|
&self,
|
||||||
|
unsigned: UnsignedEvent,
|
||||||
|
) -> BoxedFuture<'_, Result<Event, UniversalSignerError>>;
|
||||||
|
fn nip44_encrypt_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
public_key: &'a PublicKey,
|
||||||
|
content: &'a str,
|
||||||
|
) -> BoxedFuture<'a, Result<String, UniversalSignerError>>;
|
||||||
|
fn nip44_decrypt_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
public_key: &'a PublicKey,
|
||||||
|
payload: &'a str,
|
||||||
|
) -> BoxedFuture<'a, Result<String, UniversalSignerError>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct InnerSignerImpl<T>(T);
|
||||||
|
|
||||||
|
impl<T> InnerSigner for InnerSignerImpl<T>
|
||||||
|
where
|
||||||
|
T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + Send + Sync + 'static,
|
||||||
|
<T as AsyncGetPublicKey>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncSignEvent>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncNip44>::Error: Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn get_public_key_async(&self) -> BoxedFuture<'_, Result<PublicKey, UniversalSignerError>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
AsyncGetPublicKey::get_public_key_async(&self.0)
|
||||||
|
.await
|
||||||
|
.map_err(UniversalSignerError::new)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_event_async(
|
||||||
|
&self,
|
||||||
|
unsigned: UnsignedEvent,
|
||||||
|
) -> BoxedFuture<'_, Result<Event, UniversalSignerError>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
AsyncSignEvent::sign_event_async(&self.0, unsigned)
|
||||||
|
.await
|
||||||
|
.map_err(UniversalSignerError::new)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nip44_encrypt_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
public_key: &'a PublicKey,
|
||||||
|
content: &'a str,
|
||||||
|
) -> BoxedFuture<'a, Result<String, UniversalSignerError>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
AsyncNip44::nip44_encrypt_async(&self.0, public_key, content)
|
||||||
|
.await
|
||||||
|
.map_err(UniversalSignerError::new)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nip44_decrypt_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
public_key: &'a PublicKey,
|
||||||
|
payload: &'a str,
|
||||||
|
) -> BoxedFuture<'a, Result<String, UniversalSignerError>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
AsyncNip44::nip44_decrypt_async(&self.0, public_key, payload)
|
||||||
|
.await
|
||||||
|
.map_err(UniversalSignerError::new)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniversalSigner {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn with_inner<R>(&self, f: impl FnOnce(&dyn InnerSigner) -> R) -> R {
|
||||||
|
let guard = self.inner.read().expect("RwLock poisoned");
|
||||||
|
f(&**guard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncGetPublicKey for UniversalSigner {
|
||||||
|
type Error = UniversalSignerError;
|
||||||
|
|
||||||
|
fn get_public_key_async(&self) -> BoxedFuture<'_, Result<PublicKey, Self::Error>> {
|
||||||
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
|
Box::pin(async move { inner.get_public_key_async().await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncSignEvent for UniversalSigner {
|
||||||
|
type Error = UniversalSignerError;
|
||||||
|
|
||||||
|
fn sign_event_async(
|
||||||
|
&self,
|
||||||
|
unsigned: UnsignedEvent,
|
||||||
|
) -> BoxedFuture<'_, Result<Event, Self::Error>> {
|
||||||
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
|
Box::pin(async move { inner.sign_event_async(unsigned).await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncNip44 for UniversalSigner {
|
||||||
|
type Error = UniversalSignerError;
|
||||||
|
|
||||||
|
fn nip44_encrypt_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
public_key: &'a PublicKey,
|
||||||
|
content: &'a str,
|
||||||
|
) -> BoxedFuture<'a, Result<String, Self::Error>> {
|
||||||
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
|
Box::pin(async move { inner.nip44_encrypt_async(public_key, content).await })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nip44_decrypt_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
public_key: &'a PublicKey,
|
||||||
|
payload: &'a str,
|
||||||
|
) -> BoxedFuture<'a, Result<String, Self::Error>> {
|
||||||
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
|
Box::pin(async move { inner.nip44_decrypt_async(public_key, payload).await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CoopAuthUrlHandler;
|
||||||
|
|
||||||
|
impl AuthUrlHandler for CoopAuthUrlHandler {
|
||||||
|
#[allow(mismatched_lifetime_syntaxes)]
|
||||||
|
fn on_auth_url(&self, auth_url: Url) -> BoxedFuture<Result<(), nostr_connect::error::Error>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
webbrowser::open(auth_url.as_str()).unwrap();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -192,7 +192,6 @@ impl From<ThemeFamily> for Theme {
|
|||||||
let mode = ThemeMode::default();
|
let mode = ThemeMode::default();
|
||||||
|
|
||||||
// Define the font family based on the platform.
|
// Define the font family based on the platform.
|
||||||
// TODO: Use native fonts on Linux too.
|
|
||||||
let font_family = match platform {
|
let font_family = match platform {
|
||||||
PlatformKind::Linux => "Inter",
|
PlatformKind::Linux => "Inter",
|
||||||
_ => ".SystemUIFont",
|
_ => ".SystemUIFont",
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ common = { path = "../common" }
|
|||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
@@ -20,8 +18,10 @@ log.workspace = true
|
|||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
uuid = "1.10"
|
uuid = "1.10"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
image = "0.25.1"
|
|
||||||
lsp-types = "0.97.0"
|
lsp-types = "0.97.0"
|
||||||
ropey = { version = "=2.0.0-beta.1", features = ["metric_lines_lf", "metric_utf16"] }
|
ropey = { version = "=2.0.0-beta.1", features = ["metric_lines_lf", "metric_utf16"] }
|
||||||
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
||||||
tree-sitter = "0.26"
|
tree-sitter = "0.26"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, AnyElement, App, ClickEvent, Div, ElementId, Hsla, InteractiveElement,
|
AnyElement, App, ClickEvent, Div, ElementId, Hsla, InteractiveElement, IntoElement,
|
||||||
IntoElement, ParentElement, RenderOnce, SharedString, Stateful,
|
ParentElement, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _,
|
||||||
StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
StyleRefinement, Styled, Window, div, relative,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::indicator::Indicator;
|
use crate::indicator::Indicator;
|
||||||
use crate::tooltip::Tooltip;
|
use crate::tooltip::Tooltip;
|
||||||
use crate::{h_flex, Disableable, Icon, IconName, Selectable, Sizable, Size, StyledExt};
|
use crate::{Disableable, Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct ButtonCustomVariant {
|
pub struct ButtonCustomVariant {
|
||||||
@@ -617,7 +617,7 @@ impl ButtonVariant {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let fg = match self {
|
let fg = match self {
|
||||||
ButtonVariant::Primary => cx.theme().text_muted, // TODO: use a different color?
|
ButtonVariant::Primary => cx.theme().text_muted,
|
||||||
_ => cx.theme().text_muted,
|
_ => cx.theme().text_muted,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ use super::folding::FoldRange;
|
|||||||
use super::text_wrapper::{LineItem, WrapDisplayPoint};
|
use super::text_wrapper::{LineItem, WrapDisplayPoint};
|
||||||
use super::wrap_map::WrapMap;
|
use super::wrap_map::WrapMap;
|
||||||
use super::{BufferPoint, DisplayPoint};
|
use super::{BufferPoint, DisplayPoint};
|
||||||
|
use crate::input::Point as TreeSitterPoint;
|
||||||
use crate::input::display_map::WrapPoint;
|
use crate::input::display_map::WrapPoint;
|
||||||
use crate::input::rope_ext::RopeExt as _;
|
use crate::input::rope_ext::RopeExt as _;
|
||||||
use crate::input::Point as TreeSitterPoint;
|
|
||||||
|
|
||||||
/// DisplayMap is the main interface for Editor/Input coordinate mapping.
|
/// DisplayMap is the main interface for Editor/Input coordinate mapping.
|
||||||
///
|
///
|
||||||
@@ -269,10 +269,7 @@ impl DisplayMap {
|
|||||||
|
|
||||||
/// Convert wrap display point to TreeSitterPoint (buffer line/col).
|
/// Convert wrap display point to TreeSitterPoint (buffer line/col).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn wrap_display_point_to_point(
|
pub(crate) fn wrap_display_point_to_point(&self, point: WrapDisplayPoint) -> TreeSitterPoint {
|
||||||
&self,
|
|
||||||
point: WrapDisplayPoint,
|
|
||||||
) -> TreeSitterPoint {
|
|
||||||
self.wrap_map.wrapper().display_point_to_point(point)
|
self.wrap_map.wrapper().display_point_to_point(point)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use gpui::{
|
|||||||
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task,
|
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task,
|
||||||
UniformListScrollHandle, Window, div, px, size, uniform_list,
|
UniformListScrollHandle, Window, div, px, size, uniform_list,
|
||||||
};
|
};
|
||||||
use smol::Timer;
|
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
||||||
@@ -265,6 +264,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.set_searching(true, window, cx);
|
self.set_searching(true, window, cx);
|
||||||
|
|
||||||
let search = self.delegate.perform_search(&text, window, cx);
|
let search = self.delegate.perform_search(&text, window, cx);
|
||||||
|
|
||||||
if self.rows_cache.len() > 0 {
|
if self.rows_cache.len() > 0 {
|
||||||
@@ -273,6 +273,7 @@ where
|
|||||||
self._set_selected_index(None, window, cx);
|
self._set_selected_index(None, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
self._search_task = cx.spawn_in(window, async move |this, window| {
|
self._search_task = cx.spawn_in(window, async move |this, window| {
|
||||||
search.await;
|
search.await;
|
||||||
|
|
||||||
@@ -282,7 +283,8 @@ where
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Always wait 100ms to avoid flicker
|
// Always wait 100ms to avoid flicker
|
||||||
Timer::after(Duration::from_millis(100)).await;
|
executor.timer(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
_ = this.update_in(window, |this, window, cx| {
|
_ = this.update_in(window, |this, window, cx| {
|
||||||
this.set_searching(false, window, cx);
|
this.set_searching(false, window, cx);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "relay_auth"
|
name = "workspace"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
state = { path = "../state" }
|
|
||||||
settings = { path = "../settings" }
|
|
||||||
common = { path = "../common" }
|
|
||||||
theme = { path = "../theme" }
|
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
common = { path = "../common" }
|
||||||
|
state = { path = "../state" }
|
||||||
|
device = { path = "../device" }
|
||||||
|
chat = { path = "../chat" }
|
||||||
|
chat_ui = { path = "../chat_ui" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
person = { path = "../person" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
nostr-connect.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smallvec.workspace = true
|
serde.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
flume.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
@@ -7,11 +7,11 @@ use gpui::{
|
|||||||
Subscription, Task, Window, div,
|
Subscription, Task, Window, div,
|
||||||
};
|
};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use state::NostrRegistry;
|
use state::{CoopAuthUrlHandler, NostrRegistry, USER_KEYRING};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{Input, InputEvent, InputState};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::{Disableable, WindowExtension, v_flex};
|
use ui::{Disableable, StyledExt, WindowExtension, v_flex};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ImportIdentity {
|
pub struct ImportIdentity {
|
||||||
@@ -36,7 +36,7 @@ pub struct ImportIdentity {
|
|||||||
|
|
||||||
impl ImportIdentity {
|
impl ImportIdentity {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let key_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
let key_input = cx.new(|cx| InputState::new(window, cx));
|
||||||
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
let error = cx.new(|_| None);
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
@@ -61,11 +61,26 @@ impl ImportIdentity {
|
|||||||
let value = self.key_input.read(cx).value();
|
let value = self.key_input.read(cx).value();
|
||||||
let password = self.pass_input.read(cx).value();
|
let password = self.pass_input.read(cx).value();
|
||||||
|
|
||||||
|
// Set loading state
|
||||||
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
if value.starts_with("ncryptsec1") {
|
if value.starts_with("ncryptsec1") {
|
||||||
self.ncryptsec(value, password, window, cx);
|
self.ncryptsec(value, password, window, cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if value.starts_with("bunker://") {
|
||||||
|
match NostrConnectUri::parse(value) {
|
||||||
|
Ok(uri) => {
|
||||||
|
self.bunker(uri, window, cx);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.set_error(e.to_string(), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(secret) = SecretKey::parse(&value) {
|
if let Ok(secret) = SecretKey::parse(&value) {
|
||||||
let keys = Keys::new(secret);
|
let keys = Keys::new(secret);
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
@@ -74,7 +89,6 @@ impl ImportIdentity {
|
|||||||
nostr.update(cx, |this, cx| {
|
nostr.update(cx, |this, cx| {
|
||||||
this.set_signer(keys, cx);
|
this.set_signer(keys, cx);
|
||||||
});
|
});
|
||||||
window.close_modal(cx);
|
|
||||||
} else {
|
} else {
|
||||||
self.set_error("Invalid key", cx);
|
self.set_error("Invalid key", cx);
|
||||||
}
|
}
|
||||||
@@ -126,10 +140,44 @@ impl ImportIdentity {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bunker(&mut self, uri: NostrConnectUri, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let master_keys = nostr.read(cx).get_master_key(cx);
|
||||||
|
let password = uri.to_string();
|
||||||
|
let save = cx.write_credentials(USER_KEYRING, "bunker", password.as_bytes());
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |_this, cx| {
|
||||||
|
let keys = master_keys.await;
|
||||||
|
let timeout = Duration::from_secs(30);
|
||||||
|
|
||||||
|
// Construct the nostr connect signer
|
||||||
|
let mut signer = NostrConnect::new(uri, keys, timeout, None)?;
|
||||||
|
|
||||||
|
// Handle auth url with the default browser
|
||||||
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.set_signer(signer, cx);
|
||||||
|
cx.background_spawn(async move { save.await.ok() }).detach();
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
|
self.loading = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
where
|
where
|
||||||
S: Into<SharedString>,
|
S: Into<SharedString>,
|
||||||
{
|
{
|
||||||
|
self.set_loading(false, cx);
|
||||||
|
|
||||||
// Update error message
|
// Update error message
|
||||||
self.error.update(cx, |this, cx| {
|
self.error.update(cx, |this, cx| {
|
||||||
*this = Some(message.into());
|
*this = Some(message.into());
|
||||||
@@ -154,34 +202,40 @@ impl ImportIdentity {
|
|||||||
|
|
||||||
impl Render for ImportIdentity {
|
impl Render for ImportIdentity {
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
const MSG: &str = "Coop isn't stored your identity secret in local device. Everything will be reset on the next login.";
|
const MSG: &str = "Coop won't store your identity key on the local device. You need to re-login again in the next session. You can use Nostr Connect for persistent login.";
|
||||||
|
|
||||||
|
let require_password = self.key_input.read(cx).value().starts_with("ncryptsec1");
|
||||||
|
let key_warning = self.key_input.read(cx).value().starts_with("nsec1") || require_password;
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.text_sm()
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child("nsec or ncryptsec://")
|
.child("Continue with existing key or bunker connection")
|
||||||
.child(Input::new(&self.key_input)),
|
.child(Input::new(&self.key_input)),
|
||||||
)
|
)
|
||||||
.when(
|
.when(require_password, |this| {
|
||||||
self.key_input.read(cx).value().starts_with("ncryptsec1"),
|
|
||||||
|this| {
|
|
||||||
this.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child("Password:")
|
.child("Decrypt Password:")
|
||||||
.child(Input::new(&self.pass_input)),
|
.child(Input::new(&self.pass_input)),
|
||||||
)
|
)
|
||||||
},
|
})
|
||||||
|
.when(key_warning, |this| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.v_flex()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_warning)
|
||||||
|
.child(div().font_semibold().child("Warning"))
|
||||||
|
.child(div().child(MSG)),
|
||||||
)
|
)
|
||||||
.child(div().text_xs().text_color(cx.theme().text_muted).child(MSG))
|
})
|
||||||
.child(
|
.child(
|
||||||
Button::new("login")
|
Button::new("login")
|
||||||
.label("Continue")
|
.label("Continue")
|
||||||
@@ -7,7 +7,7 @@ use gpui::{
|
|||||||
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
Subscription, Task, Window, div,
|
Subscription, Task, Window, div,
|
||||||
};
|
};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{Input, InputEvent, InputState};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
@@ -78,7 +78,7 @@ impl Screening {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let public_key = self.public_key;
|
let public_key = self.public_key;
|
||||||
|
|
||||||
let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(current_user) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ impl Screening {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let public_key = self.public_key;
|
let public_key = self.public_key;
|
||||||
|
|
||||||
let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(current_user) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -224,12 +224,9 @@ impl Screening {
|
|||||||
fn report(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn report(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = self.public_key;
|
let public_key = self.public_key;
|
||||||
|
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let tag = Nip56Tag::PublicKey {
|
let tag = Nip56Tag::PublicKey {
|
||||||
public_key,
|
public_key,
|
||||||
@@ -27,14 +27,16 @@ use crate::dialogs::import::ImportIdentity;
|
|||||||
use crate::dialogs::restore::RestoreEncryption;
|
use crate::dialogs::restore::RestoreEncryption;
|
||||||
use crate::dialogs::settings;
|
use crate::dialogs::settings;
|
||||||
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list, trash};
|
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list, trash};
|
||||||
use crate::sidebar;
|
|
||||||
|
mod dialogs;
|
||||||
|
mod panels;
|
||||||
|
mod sidebar;
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
||||||
cx.new(|cx| Workspace::new(window, cx))
|
cx.new(|cx| Workspace::new(window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeviceNotifcation;
|
struct DeviceNotifcation;
|
||||||
struct RelayNotifcation;
|
|
||||||
struct MsgRelayNotification;
|
struct MsgRelayNotification;
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
@@ -72,13 +74,21 @@ impl Workspace {
|
|||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let device = DeviceRegistry::global(cx);
|
let device = DeviceRegistry::global(cx);
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let signer = nostr.read(cx).signer.clone();
|
|
||||||
|
|
||||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||||
let image_cache = CoopImageCache::new(IMAGE_CACHE_SIZE, cx);
|
let image_cache = CoopImageCache::new(IMAGE_CACHE_SIZE, cx);
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Observe sign in state changes
|
||||||
|
cx.observe_in(&nostr, window, move |this, nostr, window, cx| {
|
||||||
|
if nostr.read(cx).current_user().is_some() {
|
||||||
|
this.set_center_layout(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe system appearance and update theme
|
// Observe system appearance and update theme
|
||||||
cx.observe_window_appearance(window, |_this, window, cx| {
|
cx.observe_window_appearance(window, |_this, window, cx| {
|
||||||
@@ -86,41 +96,20 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Observe the signer
|
|
||||||
cx.observe_in(&signer, window, |this, signer, window, cx| {
|
|
||||||
if signer.read(cx).is_some() {
|
|
||||||
this.set_center_layout(window, cx);
|
|
||||||
} else {
|
|
||||||
this.import_identity(window, cx);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe to the nostr events
|
// Subscribe to the nostr events
|
||||||
cx.subscribe_in(&nostr, window, move |this, state, event, window, cx| {
|
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
|
||||||
match event {
|
match event {
|
||||||
StateEvent::Connecting => {
|
StateEvent::SignerChanged => {
|
||||||
let note = Notification::new()
|
this.set_center_layout(window, cx);
|
||||||
.id::<RelayNotifcation>()
|
|
||||||
.message("Connecting to the bootstrap relays...")
|
|
||||||
.with_kind(NotificationKind::Info);
|
|
||||||
|
|
||||||
window.push_notification(note, cx);
|
cx.defer_in(window, |_this, window, cx| {
|
||||||
|
window.close_all_modals(cx);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
StateEvent::Connected => {
|
StateEvent::NoSigner => {
|
||||||
let note = Notification::new()
|
|
||||||
.id::<RelayNotifcation>()
|
|
||||||
.message("Connected to the bootstrap relays")
|
|
||||||
.with_kind(NotificationKind::Success);
|
|
||||||
|
|
||||||
window.push_notification(note, cx);
|
|
||||||
|
|
||||||
if state.read(cx).signer.read(cx).is_none() {
|
|
||||||
this.import_identity(window, cx);
|
this.import_identity(window, cx);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -327,7 +316,7 @@ impl Workspace {
|
|||||||
Command::ShowProfile => {
|
Command::ShowProfile => {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
if let Some(public_key) = nostr.read(cx).signer_pubkey(cx) {
|
if let Some(public_key) = nostr.read(cx).current_user() {
|
||||||
self.dock.update(cx, |this, cx| {
|
self.dock.update(cx, |this, cx| {
|
||||||
this.add_panel(
|
this.add_panel(
|
||||||
Arc::new(profile::init(public_key, window, cx)),
|
Arc::new(profile::init(public_key, window, cx)),
|
||||||
@@ -580,7 +569,7 @@ impl Workspace {
|
|||||||
|
|
||||||
fn titlebar_left(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn titlebar_left(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let current_user = nostr.read(cx).signer_pubkey(cx);
|
let current_user = nostr.read(cx).current_user();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
@@ -658,7 +647,7 @@ impl Workspace {
|
|||||||
let is_nip4e_enabled = AppSettings::get_nip4e(cx);
|
let is_nip4e_enabled = AppSettings::get_nip4e(cx);
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return div();
|
return div();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ use gpui::{
|
|||||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
|
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::KEYRING;
|
use state::USER_KEYRING;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
@@ -59,7 +59,7 @@ impl BackupPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let keyring = cx.read_credentials(KEYRING);
|
let keyring = cx.read_credentials(USER_KEYRING);
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
if let Some((_, secret)) = keyring.await? {
|
if let Some((_, secret)) = keyring.await? {
|
||||||
@@ -82,7 +82,7 @@ impl ContactListPanel {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -157,10 +157,7 @@ impl ContactListPanel {
|
|||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get contacts
|
// Get contacts
|
||||||
let contacts: Vec<Contact> = self
|
let contacts: Vec<Contact> = self
|
||||||
@@ -8,8 +8,8 @@ use ui::button::{Button, ButtonVariants};
|
|||||||
use ui::dock::{DockPlacement, Panel, PanelEvent};
|
use ui::dock::{DockPlacement, Panel, PanelEvent};
|
||||||
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
|
use crate::Workspace;
|
||||||
use crate::panels::profile;
|
use crate::panels::profile;
|
||||||
use crate::workspace::Workspace;
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
||||||
cx.new(|cx| GreeterPanel::new(window, cx))
|
cx.new(|cx| GreeterPanel::new(window, cx))
|
||||||
@@ -31,7 +31,7 @@ impl GreeterPanel {
|
|||||||
fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
if let Some(public_key) = nostr.read(cx).signer_pubkey(cx) {
|
if let Some(public_key) = nostr.read(cx).current_user() {
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
cx.spawn_in(window, async move |_this, cx| {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
Workspace::add_panel(
|
Workspace::add_panel(
|
||||||
@@ -83,7 +83,7 @@ impl MessagingRelayPanel {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,10 +171,7 @@ impl MessagingRelayPanel {
|
|||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Construct event tags
|
// Construct event tags
|
||||||
let tags: Vec<Tag> = self
|
let tags: Vec<Tag> = self
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
use anyhow::{Context as AnyhowContext, Error};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||||
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
|
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
|
||||||
@@ -207,12 +207,9 @@ impl ProfilePanel {
|
|||||||
fn publish(&self, metadata: &Metadata, cx: &App) -> Task<Result<(), Error>> {
|
fn publish(&self, metadata: &Metadata, cx: &App) -> Task<Result<(), Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let metadata = metadata.clone();
|
let metadata = metadata.clone();
|
||||||
|
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return Task::ready(Err(anyhow!("Signer is required")));
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
// Build and sign the metadata event
|
// Build and sign the metadata event
|
||||||
let event = metadata.finalize_async(&signer).await?;
|
let event = metadata.finalize_async(&signer).await?;
|
||||||
@@ -100,7 +100,7 @@ impl RelayListPanel {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -207,10 +207,7 @@ impl RelayListPanel {
|
|||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
let Some(signer) = nostr.read(cx).signer(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get all relays
|
// Get all relays
|
||||||
let relays = self.relays.clone();
|
let relays = self.relays.clone();
|
||||||
@@ -159,7 +159,7 @@ impl Sidebar {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ impl Sidebar {
|
|||||||
let async_chat = chat.downgrade();
|
let async_chat = chat.downgrade();
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else {
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -462,7 +462,7 @@ impl Sidebar {
|
|||||||
});
|
});
|
||||||
|
|
||||||
RoomEntry::new(range.start + ix)
|
RoomEntry::new(range.start + ix)
|
||||||
.name(profile.name())
|
.name(profile.name().trim())
|
||||||
.avatar(profile.avatar())
|
.avatar(profile.avatar())
|
||||||
.on_click(handler)
|
.on_click(handler)
|
||||||
.selected(selected)
|
.selected(selected)
|
||||||
@@ -28,43 +28,23 @@ icons = [
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
assets = { path = "../crates/assets" }
|
assets = { path = "../crates/assets" }
|
||||||
|
workspace = { path = "../crates/workspace" }
|
||||||
ui = { path = "../crates/ui" }
|
ui = { path = "../crates/ui" }
|
||||||
theme = { path = "../crates/theme" }
|
theme = { path = "../crates/theme" }
|
||||||
common = { path = "../crates/common" }
|
common = { path = "../crates/common" }
|
||||||
state = { path = "../crates/state" }
|
state = { path = "../crates/state" }
|
||||||
device = { path = "../crates/device" }
|
device = { path = "../crates/device" }
|
||||||
chat = { path = "../crates/chat" }
|
chat = { path = "../crates/chat" }
|
||||||
chat_ui = { path = "../crates/chat_ui" }
|
|
||||||
settings = { path = "../crates/settings" }
|
settings = { path = "../crates/settings" }
|
||||||
auto_update = { path = "../crates/auto_update" }
|
auto_update = { path = "../crates/auto_update" }
|
||||||
person = { path = "../crates/person" }
|
person = { path = "../crates/person" }
|
||||||
relay_auth = { path = "../crates/relay_auth" }
|
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_platform.workspace = true
|
gpui_platform.workspace = true
|
||||||
gpui_linux.workspace = true
|
gpui_linux.workspace = true
|
||||||
gpui_windows.workspace = true
|
gpui_windows.workspace = true
|
||||||
gpui_macos.workspace = true
|
gpui_macos.workspace = true
|
||||||
gpui_tokio.workspace = true
|
|
||||||
reqwest_client.workspace = true
|
reqwest_client.workspace = true
|
||||||
|
|
||||||
nostr-connect.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
|
||||||
|
|
||||||
anyhow.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
itertools.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
|
||||||
smol.workspace = true
|
|
||||||
futures.workspace = true
|
|
||||||
oneshot.workspace = true
|
|
||||||
webbrowser.workspace = true
|
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
|
||||||
indexset = "0.12.3"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
|
||||||
# Temporary workaround https://github.com/zed-industries/zed/issues/47168
|
|
||||||
core-text = "=21.0.0"
|
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ use gpui_platform::application;
|
|||||||
use state::{APP_ID, CLIENT_NAME};
|
use state::{APP_ID, CLIENT_NAME};
|
||||||
use ui::Root;
|
use ui::Root;
|
||||||
|
|
||||||
mod dialogs;
|
|
||||||
mod panels;
|
|
||||||
mod sidebar;
|
|
||||||
mod workspace;
|
|
||||||
|
|
||||||
actions!(coop, [Quit]);
|
actions!(coop, [Quit]);
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -71,9 +66,6 @@ fn main() {
|
|||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
// Initialize the tokio runtime
|
|
||||||
gpui_tokio::init(cx);
|
|
||||||
|
|
||||||
// Initialize components
|
// Initialize components
|
||||||
ui::init(cx);
|
ui::init(cx);
|
||||||
|
|
||||||
@@ -89,9 +81,6 @@ fn main() {
|
|||||||
// Initialize person registry
|
// Initialize person registry
|
||||||
person::init(window, cx);
|
person::init(window, cx);
|
||||||
|
|
||||||
// Initialize relay auth registry
|
|
||||||
relay_auth::init(window, cx);
|
|
||||||
|
|
||||||
// Initialize device signer
|
// Initialize device signer
|
||||||
//
|
//
|
||||||
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ targets = [
|
|||||||
"aarch64-pc-windows-msvc",
|
"aarch64-pc-windows-msvc",
|
||||||
"aarch64-apple-ios",
|
"aarch64-apple-ios",
|
||||||
"aarch64-linux-android",
|
"aarch64-linux-android",
|
||||||
"wasm32-unknown-unknown"
|
"wasm32-unknown-unknown",
|
||||||
|
"wasm32-wasip2"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
workspace = { path = "../crates/workspace" }
|
||||||
assets = { path = "../crates/assets" }
|
assets = { path = "../crates/assets" }
|
||||||
ui = { path = "../crates/ui" }
|
ui = { path = "../crates/ui" }
|
||||||
theme = { path = "../crates/theme" }
|
theme = { path = "../crates/theme" }
|
||||||
@@ -14,29 +15,21 @@ device = { path = "../crates/device" }
|
|||||||
chat = { path = "../crates/chat" }
|
chat = { path = "../crates/chat" }
|
||||||
settings = { path = "../crates/settings" }
|
settings = { path = "../crates/settings" }
|
||||||
person = { path = "../crates/person" }
|
person = { path = "../crates/person" }
|
||||||
relay_auth = { path = "../crates/relay_auth" }
|
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_platform.workspace = true
|
gpui_platform.workspace = true
|
||||||
gpui_tokio.workspace = true
|
|
||||||
gpui_web = { git = "https://github.com/zed-industries/zed" }
|
gpui_web = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
nostr-connect.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
|
||||||
|
|
||||||
anyhow.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
itertools.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
|
||||||
smol.workspace = true
|
|
||||||
futures.workspace = true
|
|
||||||
oneshot.workspace = true
|
|
||||||
webbrowser.workspace = true
|
|
||||||
tracing-subscriber.workspace = true
|
|
||||||
|
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
tracing-wasm = "0.2"
|
tracing-wasm = "0.2"
|
||||||
console_log = "1.0"
|
console_log = "1.0"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
getrandom_0_2 = { package = "getrandom", version = "0.2", features = ["js"] }
|
||||||
|
getrandom_0_3 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
|
||||||
|
getrandom_0_4 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }
|
||||||
|
tokio = { version = "1", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] }
|
||||||
|
errno = { version = "0.3.14", default-features = false }
|
||||||
|
|||||||
46
web/Makefile
Normal file
46
web/Makefile
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
.PHONY: help dev build-wasm build-web build clean install
|
||||||
|
|
||||||
|
help: ## Show help information
|
||||||
|
@echo "Coop - Available commands:"
|
||||||
|
@echo ""
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
install: ## Install all dependencies
|
||||||
|
@echo "Checking Rust WASM target..."
|
||||||
|
@rustup target add wasm32-unknown-unknown || true
|
||||||
|
@echo "Checking wasm-bindgen-cli..."
|
||||||
|
@cargo install wasm-bindgen-cli || true
|
||||||
|
@echo "Installing frontend dependencies..."
|
||||||
|
@cd www && deno install
|
||||||
|
|
||||||
|
build-wasm: ## Build WASM (release mode)
|
||||||
|
@./script/build-wasm.sh --release
|
||||||
|
|
||||||
|
build-wasm-dev: ## Build WASM (debug mode)
|
||||||
|
@./script/build-wasm.sh
|
||||||
|
|
||||||
|
build-web: ## Build frontend
|
||||||
|
@cd www && deno task build
|
||||||
|
|
||||||
|
build-web-prod: ## Build frontend for production (GitHub Pages)
|
||||||
|
@cd www && deno install && NODE_ENV=production deno task build
|
||||||
|
|
||||||
|
build: build-wasm build-web ## Build complete project (WASM + frontend)
|
||||||
|
|
||||||
|
build-prod: build-wasm build-web-prod ## Build complete project for production
|
||||||
|
|
||||||
|
dev: build-wasm-dev ## Start development server
|
||||||
|
@cd www && deno install && deno task dev
|
||||||
|
|
||||||
|
preview: ## Preview production build
|
||||||
|
@cd www && deno task preview
|
||||||
|
|
||||||
|
clean: ## Clean build artifacts
|
||||||
|
@echo "Cleaning build artifacts..."
|
||||||
|
@rm -rf www/dist
|
||||||
|
@rm -rf www/src/wasm/*.js www/src/wasm/*.wasm
|
||||||
|
@cargo clean
|
||||||
|
|
||||||
|
watch-wasm: ## Watch Rust code changes and auto-rebuild WASM
|
||||||
|
@echo "Watching WASM changes..."
|
||||||
|
@cargo watch -x 'build --target wasm32-unknown-unknown' -s './script/build-wasm.sh'
|
||||||
4
web/rust-toolchain.toml
Normal file
4
web/rust-toolchain.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
|
components = ["rustfmt", "clippy"]
|
||||||
|
targets = ["wasm32-unknown-unknown"]
|
||||||
56
web/script/build-wasm.sh
Executable file
56
web/script/build-wasm.sh
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${GREEN}Building Coop Web...${NC}"
|
||||||
|
|
||||||
|
# Get the script directory
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
PROJECT_ROOT="$SCRIPT_DIR/.."
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
RELEASE_FLAG=""
|
||||||
|
if [[ "$1" == "--release" ]]; then
|
||||||
|
RELEASE_FLAG="--release"
|
||||||
|
echo -e "${YELLOW}Building in release mode${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 1: Build WASM
|
||||||
|
echo -e "${GREEN}Step 1: Building WASM...${NC}"
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
cargo build --target wasm32-unknown-unknown $RELEASE_FLAG
|
||||||
|
|
||||||
|
# Determine the build directory
|
||||||
|
if [[ "$RELEASE_FLAG" == "--release" ]]; then
|
||||||
|
BUILD_MODE="release"
|
||||||
|
else
|
||||||
|
BUILD_MODE="debug"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# WASM file is in workspace target directory
|
||||||
|
WORKSPACE_ROOT="$PROJECT_ROOT/../.."
|
||||||
|
WASM_PATH="$WORKSPACE_ROOT/target/wasm32-unknown-unknown/$BUILD_MODE/coop_web.wasm"
|
||||||
|
|
||||||
|
# Check if WASM file exists
|
||||||
|
if [[ ! -f "$WASM_PATH" ]]; then
|
||||||
|
echo -e "${RED}Error: WASM file not found at: $WASM_PATH${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2: Generate JavaScript bindings
|
||||||
|
echo -e "${GREEN}Step 2: Generating JavaScript bindings...${NC}"
|
||||||
|
wasm-bindgen "$WASM_PATH" \
|
||||||
|
--out-dir "$PROJECT_ROOT/www/src/wasm" \
|
||||||
|
--target web \
|
||||||
|
--no-typescript
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Build completed successfully!${NC}"
|
||||||
|
echo -e "${YELLOW}Next steps:${NC}"
|
||||||
|
echo -e " cd www"
|
||||||
|
echo -e " deno install"
|
||||||
|
echo -e " deno run dev"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use gpui::*;
|
use gpui::*;
|
||||||
|
use ui::Root;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -14,13 +15,39 @@ pub fn run() -> Result<(), JsValue> {
|
|||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
gpui_platform::web_init();
|
gpui_platform::web_init();
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
gpui_platform::application().run(|cx| {
|
||||||
let app = gpui_platform::application();
|
cx.open_window(WindowOptions::default(), |window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
// Initialize components
|
||||||
|
ui::init(cx);
|
||||||
|
|
||||||
#[cfg(target_family = "wasm")]
|
// Initialize theme registry
|
||||||
let app = gpui_platform::single_threaded_web();
|
theme::init(cx);
|
||||||
|
|
||||||
app.run(|_cx| {});
|
// Initialize settings
|
||||||
|
settings::init(window, cx);
|
||||||
|
|
||||||
|
// Initialize the nostr client
|
||||||
|
state::init(window, cx);
|
||||||
|
|
||||||
|
// Initialize person registry
|
||||||
|
person::init(window, cx);
|
||||||
|
|
||||||
|
// Initialize device signer
|
||||||
|
//
|
||||||
|
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
device::init(window, cx);
|
||||||
|
|
||||||
|
// Initialize app registry
|
||||||
|
chat::init(window, cx);
|
||||||
|
|
||||||
|
// Root Entity
|
||||||
|
Root::new(workspace::init(window, cx).into(), window, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.expect("Failed to open window. Please restart the application.");
|
||||||
|
cx.activate(true);
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,6 @@
|
|||||||
<p>Loading Coop...</p>
|
<p>Loading Coop...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
35
web/src/www/main.js
Normal file
35
web/src/www/main.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
async function init() {
|
||||||
|
const loadingEl = document.getElementById('loading');
|
||||||
|
const appEl = document.getElementById('app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Import the WASM module
|
||||||
|
const wasm = await import('./wasm/coop_web.js');
|
||||||
|
await wasm.default();
|
||||||
|
|
||||||
|
// Initialize the story gallery
|
||||||
|
await wasm.run();
|
||||||
|
|
||||||
|
// Hide loading indicator
|
||||||
|
if (appEl) {
|
||||||
|
appEl.remove();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize:', error);
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.innerHTML = `
|
||||||
|
<div class="error">
|
||||||
|
<h2>Failed to load the application</h2>
|
||||||
|
<p>${error.message || error}</p>
|
||||||
|
<p style="margin-top: 10px; font-size: 14px;">
|
||||||
|
Please check the console for more details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
17
web/src/www/package.json
Normal file
17
web/src/www/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "coop-web",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"wasm": "cargo build --target wasm32-unknown-unknown --release && wasm-bindgen ../../../target/wasm32-unknown-unknown/release/gpui_component_story_web.wasm --out-dir ./src/wasm --target web --no-typescript"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^8",
|
||||||
|
"vite-plugin-static-copy": "^3.2.0",
|
||||||
|
"vite-plugin-wasm": "^3.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
75
web/src/www/vite.config.js
Normal file
75
web/src/www/vite.config.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import wasm from "vite-plugin-wasm";
|
||||||
|
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
wasm(),
|
||||||
|
viteStaticCopy({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
src: path.resolve(__dirname, "../../../assets/icons"),
|
||||||
|
dest: "assets",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "serve-assets",
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use(
|
||||||
|
"/coop/assets",
|
||||||
|
(req, res, next) => {
|
||||||
|
const assetsPath = path.resolve(__dirname, "../../../assets");
|
||||||
|
const filePath = path.join(
|
||||||
|
assetsPath,
|
||||||
|
req.url.replace("/assets", ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to serve the file
|
||||||
|
import("fs").then(({ default: fs }) => {
|
||||||
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
if (filePath.endsWith(".svg")) {
|
||||||
|
res.setHeader("Content-Type", "image/svg+xml");
|
||||||
|
}
|
||||||
|
fs.createReadStream(filePath).pipe(res);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
target: "esnext",
|
||||||
|
minify: true,
|
||||||
|
sourcemap: false,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: true,
|
||||||
|
fs: {
|
||||||
|
strict: false,
|
||||||
|
allow: [".."],
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||||
|
"Cross-Origin-Opener-Policy": "same-origin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ["./src/wasm"],
|
||||||
|
},
|
||||||
|
base: "/",
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user