feat: refactor to use gpui event instead of local state #18
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -4299,6 +4299,17 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nostr-memory"
|
||||||
|
version = "0.44.0"
|
||||||
|
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
|
||||||
|
dependencies = [
|
||||||
|
"btreecap",
|
||||||
|
"nostr",
|
||||||
|
"nostr-database",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.44.1"
|
version = "0.44.1"
|
||||||
@@ -4759,6 +4770,7 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"state",
|
"state",
|
||||||
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6434,6 +6446,7 @@ dependencies = [
|
|||||||
"nostr-connect",
|
"nostr-connect",
|
||||||
"nostr-gossip-sqlite",
|
"nostr-gossip-sqlite",
|
||||||
"nostr-lmdb",
|
"nostr-lmdb",
|
||||||
|
"nostr-memory",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"petname",
|
"petname",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -7403,6 +7416,12 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usvg"
|
name = "usvg"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ 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-memory = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-connect = { 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-sqlite = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ use common::EventUtils;
|
|||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
|
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
|
||||||
|
WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{DEVICE_GIFTWRAP, NostrRegistry, RelayState, TIMEOUT, USER_GIFTWRAP};
|
use state::{DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, USER_GIFTWRAP};
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
mod room;
|
mod room;
|
||||||
@@ -39,6 +40,10 @@ pub enum ChatEvent {
|
|||||||
CloseRoom(u64),
|
CloseRoom(u64),
|
||||||
/// An event to notify UI about a new chat request
|
/// An event to notify UI about a new chat request
|
||||||
Ping,
|
Ping,
|
||||||
|
/// An event to notify UI that the chat registry has subscribed to messaging relays
|
||||||
|
Subscribed,
|
||||||
|
/// An error occurred
|
||||||
|
Error(SharedString),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Channel signal.
|
/// Channel signal.
|
||||||
@@ -48,41 +53,25 @@ enum Signal {
|
|||||||
Message(NewMessage),
|
Message(NewMessage),
|
||||||
/// Eose received from relay pool
|
/// Eose received from relay pool
|
||||||
Eose,
|
Eose,
|
||||||
}
|
/// An error occurred
|
||||||
|
Error(SharedString),
|
||||||
/// Inbox state.
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum InboxState {
|
|
||||||
#[default]
|
|
||||||
Idle,
|
|
||||||
Checking,
|
|
||||||
RelayNotAvailable,
|
|
||||||
RelayConfigured(Box<Event>),
|
|
||||||
Subscribing,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InboxState {
|
|
||||||
pub fn not_configured(&self) -> bool {
|
|
||||||
matches!(self, InboxState::RelayNotAvailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribing(&self) -> bool {
|
|
||||||
matches!(self, InboxState::Subscribing)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Chat Registry
|
/// Chat Registry
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ChatRegistry {
|
pub struct ChatRegistry {
|
||||||
/// Relay state for messaging relay list
|
|
||||||
state: Entity<InboxState>,
|
|
||||||
|
|
||||||
/// Collection of all chat rooms
|
/// Collection of all chat rooms
|
||||||
rooms: Vec<Entity<Room>>,
|
rooms: Vec<Entity<Room>>,
|
||||||
|
|
||||||
/// Tracking the status of unwrapping gift wrap events.
|
/// Tracking the status of unwrapping gift wrap events.
|
||||||
tracking_flag: Arc<AtomicBool>,
|
tracking_flag: Arc<AtomicBool>,
|
||||||
|
|
||||||
|
/// Channel for sending signals to the UI.
|
||||||
|
signal_tx: flume::Sender<Signal>,
|
||||||
|
|
||||||
|
/// Channel for receiving signals from the UI.
|
||||||
|
signal_rx: flume::Receiver<Signal>,
|
||||||
|
|
||||||
/// Async tasks
|
/// Async tasks
|
||||||
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
||||||
|
|
||||||
@@ -105,36 +94,18 @@ 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 state = cx.new(|_| InboxState::default());
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let (tx, rx) = flume::unbounded::<Signal>();
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the nip65 state and load chat rooms on every state change
|
// Subscribe to the signer event
|
||||||
cx.observe(&nostr, |this, state, cx| {
|
cx.subscribe(&nostr, |this, _state, event, cx| {
|
||||||
match state.read(cx).relay_list_state {
|
if let StateEvent::SignerSet = event {
|
||||||
RelayState::Idle => {
|
this.reset(cx);
|
||||||
this.reset(cx);
|
this.get_rooms(cx);
|
||||||
}
|
this.get_contact_list(cx);
|
||||||
RelayState::Configured => {
|
this.get_messages(cx)
|
||||||
this.get_contact_list(cx);
|
|
||||||
this.ensure_messaging_relays(cx);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load rooms on every state change
|
|
||||||
this.get_rooms(cx);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Observe the nip17 state and load chat rooms on every state change
|
|
||||||
cx.observe(&state, |this, state, cx| {
|
|
||||||
if let InboxState::RelayConfigured(event) = state.read(cx) {
|
|
||||||
let relay_urls: Vec<_> = nip17::extract_relay_list(event).cloned().collect();
|
|
||||||
this.get_messages(relay_urls, cx);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -147,9 +118,10 @@ impl ChatRegistry {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
state,
|
|
||||||
rooms: vec![],
|
rooms: vec![],
|
||||||
tracking_flag: Arc::new(AtomicBool::new(false)),
|
tracking_flag: Arc::new(AtomicBool::new(false)),
|
||||||
|
signal_rx: rx,
|
||||||
|
signal_tx: tx,
|
||||||
tasks: smallvec![],
|
tasks: smallvec![],
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
@@ -167,7 +139,8 @@ impl ChatRegistry {
|
|||||||
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
|
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
|
||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
// Channel for communication between nostr and gpui
|
||||||
let (tx, rx) = flume::bounded::<Signal>(1024);
|
let tx = self.signal_tx.clone();
|
||||||
|
let rx = self.signal_rx.clone();
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
let device_signer = signer.get_encryption_signer().await;
|
let device_signer = signer.get_encryption_signer().await;
|
||||||
@@ -194,19 +167,29 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
// Extract the rumor from the gift wrap event
|
// Extract the rumor from the gift wrap event
|
||||||
match extract_rumor(&client, &device_signer, event.as_ref()).await {
|
match extract_rumor(&client, &device_signer, event.as_ref()).await {
|
||||||
Ok(rumor) => match rumor.created_at >= initialized_at {
|
Ok(rumor) => {
|
||||||
true => {
|
if rumor.tags.is_empty() {
|
||||||
let new_message = NewMessage::new(event.id, rumor);
|
let error: SharedString =
|
||||||
let signal = Signal::Message(new_message);
|
"Message doesn't belong to any rooms".into();
|
||||||
|
tx.send_async(Signal::Error(error)).await?;
|
||||||
|
}
|
||||||
|
|
||||||
tx.send_async(signal).await?;
|
match rumor.created_at >= initialized_at {
|
||||||
|
true => {
|
||||||
|
let new_message = NewMessage::new(event.id, rumor);
|
||||||
|
let signal = Signal::Message(new_message);
|
||||||
|
|
||||||
|
tx.send_async(signal).await?;
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
status.store(true, Ordering::Release);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
false => {
|
}
|
||||||
status.store(true, Ordering::Release);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to unwrap the gift wrap event: {e}");
|
let error: SharedString =
|
||||||
|
format!("Failed to unwrap the gift wrap event: {e}").into();
|
||||||
|
tx.send_async(Signal::Error(error)).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,6 +218,11 @@ impl ChatRegistry {
|
|||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
Signal::Error(error) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(ChatEvent::Error(error));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +233,7 @@ impl ChatRegistry {
|
|||||||
/// Tracking the status of unwrapping gift wrap events.
|
/// Tracking the status of unwrapping gift wrap events.
|
||||||
fn tracking(&mut self, cx: &mut Context<Self>) {
|
fn tracking(&mut self, cx: &mut Context<Self>) {
|
||||||
let status = self.tracking_flag.clone();
|
let status = self.tracking_flag.clone();
|
||||||
|
let tx = self.signal_tx.clone();
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
let loop_duration = Duration::from_secs(15);
|
let loop_duration = Duration::from_secs(15);
|
||||||
@@ -252,6 +241,9 @@ impl ChatRegistry {
|
|||||||
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);
|
||||||
|
_ = tx.send_async(Signal::Eose).await;
|
||||||
|
} else {
|
||||||
|
_ = tx.send_async(Signal::Eose).await;
|
||||||
}
|
}
|
||||||
smol::Timer::after(loop_duration).await;
|
smol::Timer::after(loop_duration).await;
|
||||||
}
|
}
|
||||||
@@ -289,27 +281,29 @@ impl ChatRegistry {
|
|||||||
self.tasks.push(task);
|
self.tasks.push(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure messaging relays are set up for the current user.
|
/// Get all messages for current user
|
||||||
pub fn ensure_messaging_relays(&mut self, cx: &mut Context<Self>) {
|
fn get_messages(&mut self, cx: &mut Context<Self>) {
|
||||||
let task = self.verify_relays(cx);
|
let task = self.subscribe(cx);
|
||||||
|
|
||||||
// Set state to checking
|
|
||||||
self.set_state(InboxState::Checking, cx);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
let result = task.await?;
|
match task.await {
|
||||||
|
Ok(_) => {
|
||||||
// Update state
|
this.update(cx, |_this, cx| {
|
||||||
this.update(cx, |this, cx| {
|
cx.emit(ChatEvent::Subscribed);
|
||||||
this.set_state(result, cx);
|
})?;
|
||||||
})?;
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(ChatEvent::Error(SharedString::from(e.to_string())));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify messaging relay list for current user
|
// Get messaging relay list for current user
|
||||||
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, Error>> {
|
fn get_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, 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 signer = nostr.read(cx).signer();
|
||||||
@@ -330,50 +324,25 @@ impl ChatRegistry {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
while let Some((_url, res)) = stream.next().await {
|
while let Some((_url, res)) = stream.next().await {
|
||||||
match res {
|
if let Ok(event) = res {
|
||||||
Ok(event) => {
|
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
||||||
return Ok(InboxState::RelayConfigured(Box::new(event)));
|
return Ok(urls);
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to receive relay list event: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(InboxState::RelayNotAvailable)
|
Err(anyhow!("Messaging Relays not found"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all messages for current user
|
|
||||||
fn get_messages<I>(&mut self, relay_urls: I, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = RelayUrl>,
|
|
||||||
{
|
|
||||||
let task = self.subscribe(relay_urls, cx);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
task.await?;
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_state(InboxState::Subscribing, cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Continuously get gift wrap events for the current user in their messaging relays
|
/// Continuously get gift wrap events for the current user in their messaging relays
|
||||||
fn subscribe<I>(&mut self, urls: I, cx: &mut Context<Self>) -> Task<Result<(), Error>>
|
fn subscribe(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||||
where
|
|
||||||
I: IntoIterator<Item = RelayUrl>,
|
|
||||||
{
|
|
||||||
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 signer = nostr.read(cx).signer();
|
||||||
let urls = urls.into_iter().collect::<Vec<_>>();
|
let urls = self.get_messaging_relays(cx);
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
|
let urls = urls.await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
let id = SubscriptionId::new(USER_GIFTWRAP);
|
let id = SubscriptionId::new(USER_GIFTWRAP);
|
||||||
@@ -400,19 +369,6 @@ impl ChatRegistry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the state of the inbox
|
|
||||||
fn set_state(&mut self, state: InboxState, cx: &mut Context<Self>) {
|
|
||||||
self.state.update(cx, |this, cx| {
|
|
||||||
*this = state;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the relay state
|
|
||||||
pub fn state(&self, cx: &App) -> InboxState {
|
|
||||||
self.state.read(cx).clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the loading status of the chat registry
|
/// Get the loading status of the chat registry
|
||||||
pub fn loading(&self) -> bool {
|
pub fn loading(&self) -> bool {
|
||||||
self.tracking_flag.load(Ordering::Acquire)
|
self.tracking_flag.load(Ordering::Acquire)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{Error, anyhow};
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use gpui::{Image, ImageFormat, SharedString};
|
use gpui::{Image, ImageFormat, SharedString};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render,
|
||||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, px,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use state::{NostrRegistry, SignerEvent};
|
use state::{NostrRegistry, StateEvent};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, WindowExtension};
|
use ui::{Disableable, Icon, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
use crate::dialogs::connect::ConnectSigner;
|
use crate::dialogs::connect::ConnectSigner;
|
||||||
use crate::dialogs::import::ImportKey;
|
use crate::dialogs::import::ImportKey;
|
||||||
@@ -44,13 +44,14 @@ impl AccountSelector {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
|
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
|
||||||
match event {
|
match event {
|
||||||
SignerEvent::Set => {
|
StateEvent::SignerSet => {
|
||||||
window.close_all_modals(cx);
|
window.close_all_modals(cx);
|
||||||
window.refresh();
|
window.refresh();
|
||||||
}
|
}
|
||||||
SignerEvent::Error(e) => {
|
StateEvent::Error(e) => {
|
||||||
this.set_error(e.to_string(), cx);
|
this.set_error(e.to_string(), cx);
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ use std::time::Duration;
|
|||||||
use common::TextUtils;
|
use common::TextUtils;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, px, AppContext, Context, Entity, Image, IntoElement, ParentElement, Render,
|
AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
SharedString, Styled, Subscription, Window,
|
Subscription, Window, div, img, px,
|
||||||
};
|
};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use state::{
|
use state::{
|
||||||
CoopAuthUrlHandler, NostrRegistry, SignerEvent, CLIENT_NAME, NOSTR_CONNECT_RELAY,
|
CLIENT_NAME, CoopAuthUrlHandler, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, NostrRegistry,
|
||||||
NOSTR_CONNECT_TIMEOUT,
|
StateEvent,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::v_flex;
|
use ui::v_flex;
|
||||||
@@ -31,7 +31,7 @@ impl ConnectSigner {
|
|||||||
let error = cx.new(|_| None);
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let app_keys = nostr.read(cx).app_keys.clone();
|
let app_keys = nostr.read(cx).keys();
|
||||||
|
|
||||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||||
@@ -55,7 +55,7 @@ impl ConnectSigner {
|
|||||||
|
|
||||||
// Subscribe to the signer event
|
// Subscribe to the signer event
|
||||||
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||||
if let SignerEvent::Error(e) = event {
|
if let StateEvent::Error(e) = event {
|
||||||
this.set_error(e, cx);
|
this.set_error(e, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{Error, anyhow};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
Subscription, Task, Window,
|
Subscription, Task, Window, div,
|
||||||
};
|
};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{CoopAuthUrlHandler, NostrRegistry, SignerEvent};
|
use state::{CoopAuthUrlHandler, NostrRegistry, StateEvent};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
use ui::{v_flex, Disableable};
|
use ui::{Disableable, v_flex};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ImportKey {
|
pub struct ImportKey {
|
||||||
@@ -60,7 +60,7 @@ impl ImportKey {
|
|||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe to the nostr signer event
|
// Subscribe to the nostr signer event
|
||||||
cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||||
if let SignerEvent::Error(e) = event {
|
if let StateEvent::Error(e) = event {
|
||||||
this.set_error(e, cx);
|
this.set_error(e, cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -117,7 +117,7 @@ impl ImportKey {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let app_keys = nostr.read(cx).app_keys.clone();
|
let app_keys = nostr.read(cx).keys();
|
||||||
let timeout = Duration::from_secs(30);
|
let timeout = Duration::from_secs(30);
|
||||||
|
|
||||||
// Construct the nostr connect signer
|
// Construct the nostr connect signer
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
use chat::{ChatRegistry, InboxState};
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg,
|
||||||
};
|
};
|
||||||
use state::{NostrRegistry, RelayState};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::dock::DockPlacement;
|
use ui::dock_area::dock::DockPlacement;
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
|
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
use crate::panels::{messaging_relays, profile, relay_list};
|
use crate::panels::profile;
|
||||||
use crate::workspace::Workspace;
|
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> {
|
||||||
@@ -82,15 +80,6 @@ impl Render for GreeterPanel {
|
|||||||
const TITLE: &str = "Welcome to Coop!";
|
const TITLE: &str = "Welcome to Coop!";
|
||||||
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
|
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
|
||||||
|
|
||||||
let chat = ChatRegistry::global(cx);
|
|
||||||
let nip17 = chat.read(cx).state(cx);
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let nip65 = nostr.read(cx).relay_list_state.clone();
|
|
||||||
|
|
||||||
let required_actions =
|
|
||||||
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
|
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -130,64 +119,6 @@ impl Render for GreeterPanel {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when(required_actions, |this| {
|
|
||||||
this.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.w_full()
|
|
||||||
.text_xs()
|
|
||||||
.font_semibold()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("Required Actions"))
|
|
||||||
.child(div().flex_1().h_px().bg(cx.theme().border)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.w_full()
|
|
||||||
.when(nip65.not_configured(), |this| {
|
|
||||||
this.child(
|
|
||||||
Button::new("relaylist")
|
|
||||||
.icon(Icon::new(IconName::Relay))
|
|
||||||
.label("Set up relay list")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.justify_start()
|
|
||||||
.on_click(move |_ev, window, cx| {
|
|
||||||
Workspace::add_panel(
|
|
||||||
relay_list::init(window, cx),
|
|
||||||
DockPlacement::Center,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(nip17.not_configured(), |this| {
|
|
||||||
this.child(
|
|
||||||
Button::new("import")
|
|
||||||
.icon(Icon::new(IconName::Relay))
|
|
||||||
.label("Set up messaging relays")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.justify_start()
|
|
||||||
.on_click(move |_ev, window, cx| {
|
|
||||||
Workspace::add_panel(
|
|
||||||
messaging_relays::init(window, cx),
|
|
||||||
DockPlacement::Center,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ::settings::AppSettings;
|
use ::settings::AppSettings;
|
||||||
use chat::{ChatEvent, ChatRegistry, InboxState};
|
use chat::{ChatEvent, ChatRegistry};
|
||||||
use device::DeviceRegistry;
|
use device::DeviceRegistry;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -11,7 +11,7 @@ use gpui::{
|
|||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{NostrRegistry, RelayState, SignerEvent};
|
use state::{NostrRegistry, StateEvent};
|
||||||
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
|
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
|
||||||
use title_bar::TitleBar;
|
use title_bar::TitleBar;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
@@ -96,7 +96,7 @@ impl Workspace {
|
|||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe to the signer events
|
// Subscribe to the signer events
|
||||||
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
|
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
|
||||||
if let SignerEvent::Set = event {
|
if let StateEvent::SignerSet = event {
|
||||||
this.set_center_layout(window, cx);
|
this.set_center_layout(window, cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -295,7 +295,7 @@ impl Workspace {
|
|||||||
Command::RefreshMessagingRelays => {
|
Command::RefreshMessagingRelays => {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
chat.update(cx, |this, cx| {
|
chat.update(cx, |this, cx| {
|
||||||
this.ensure_messaging_relays(cx);
|
//this.ensure_messaging_relays(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Command::ToggleTheme => {
|
Command::ToggleTheme => {
|
||||||
@@ -534,7 +534,6 @@ impl Workspace {
|
|||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let inbox_state = chat.read(cx).state(cx);
|
|
||||||
|
|
||||||
let Some(pkey) = signer.public_key() else {
|
let Some(pkey) = signer.public_key() else {
|
||||||
return div();
|
return div();
|
||||||
@@ -584,142 +583,143 @@ impl Workspace {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
/*
|
||||||
h_flex()
|
.child(
|
||||||
.gap_2()
|
h_flex()
|
||||||
.child(
|
.gap_2()
|
||||||
div()
|
.child(
|
||||||
.text_xs()
|
div()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_xs()
|
||||||
.map(|this| match inbox_state {
|
.text_color(cx.theme().text_muted)
|
||||||
InboxState::Checking => this.child(div().child(
|
.map(|this| match inbox_state {
|
||||||
SharedString::from("Fetching user's messaging relay list..."),
|
InboxState::Checking => this.child(div().child(
|
||||||
)),
|
SharedString::from("Fetching user's messaging relay list..."),
|
||||||
InboxState::RelayNotAvailable => {
|
)),
|
||||||
this.child(div().text_color(cx.theme().warning_active).child(
|
InboxState::RelayNotAvailable => {
|
||||||
SharedString::from(
|
this.child(div().text_color(cx.theme().warning_active).child(
|
||||||
"User hasn't configured a messaging relay list",
|
SharedString::from(
|
||||||
),
|
"User hasn't configured a messaging relay list",
|
||||||
))
|
),
|
||||||
}
|
))
|
||||||
_ => this,
|
}
|
||||||
}),
|
_ => this,
|
||||||
)
|
}),
|
||||||
.child(
|
)
|
||||||
Button::new("inbox")
|
.child(
|
||||||
.icon(IconName::Inbox)
|
Button::new("inbox")
|
||||||
.tooltip("Inbox")
|
.icon(IconName::Inbox)
|
||||||
.small()
|
.tooltip("Inbox")
|
||||||
.ghost()
|
.small()
|
||||||
.when(inbox_state.subscribing(), |this| this.indicator())
|
.ghost()
|
||||||
.dropdown_menu(move |this, _window, cx| {
|
.when(inbox_state.subscribing(), |this| this.indicator())
|
||||||
let persons = PersonRegistry::global(cx);
|
.dropdown_menu(move |this, _window, cx| {
|
||||||
let profile = persons.read(cx).get(&pkey, cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let urls: Vec<SharedString> = profile
|
let profile = persons.read(cx).get(&pkey, cx);
|
||||||
.messaging_relays()
|
let urls: Vec<SharedString> = profile
|
||||||
.iter()
|
.messaging_relays()
|
||||||
.map(|url| SharedString::from(url.to_string()))
|
.iter()
|
||||||
.collect();
|
.map(|url| SharedString::from(url.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
let menu = this.min_w(px(260.)).label("Messaging Relays");
|
let menu = this.min_w(px(260.)).label("Messaging Relays");
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
let menu = urls.into_iter().fold(menu, |this, url| {
|
let menu = urls.into_iter().fold(menu, |this, url| {
|
||||||
this.item(PopupMenuItem::element(move |_window, _cx| {
|
this.item(PopupMenuItem::element(move |_window, _cx| {
|
||||||
h_flex()
|
h_flex()
|
||||||
.px_1()
|
.px_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(
|
.child(
|
||||||
div().size_1p5().rounded_full().bg(gpui::green()),
|
div().size_1p5().rounded_full().bg(gpui::green()),
|
||||||
)
|
)
|
||||||
.child(url.clone())
|
.child(url.clone())
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
menu.separator()
|
menu.separator()
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
"Reload",
|
"Reload",
|
||||||
IconName::Refresh,
|
IconName::Refresh,
|
||||||
Box::new(Command::RefreshMessagingRelays),
|
Box::new(Command::RefreshMessagingRelays),
|
||||||
)
|
)
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
"Update relays",
|
"Update relays",
|
||||||
IconName::Settings,
|
IconName::Settings,
|
||||||
Box::new(Command::ShowMessaging),
|
Box::new(Command::ShowMessaging),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.map(|this| match nostr.read(cx).relay_list_state {
|
.map(|this| match nostr.read(cx).relay_list_state {
|
||||||
RelayState::Checking => this
|
RelayState::Checking => this
|
||||||
.child(div().child(SharedString::from(
|
.child(div().child(SharedString::from(
|
||||||
"Fetching user's relay list...",
|
"Fetching user's relay list...",
|
||||||
))),
|
))),
|
||||||
RelayState::NotConfigured => {
|
RelayState::NotConfigured => {
|
||||||
this.child(div().text_color(cx.theme().warning_active).child(
|
this.child(div().text_color(cx.theme().warning_active).child(
|
||||||
SharedString::from("User hasn't configured a relay list"),
|
SharedString::from("User hasn't configured a relay list"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
_ => this,
|
_ => this,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("relay-list")
|
Button::new("relay-list")
|
||||||
.icon(IconName::Relay)
|
.icon(IconName::Relay)
|
||||||
.tooltip("User's relay list")
|
.tooltip("User's relay list")
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.when(nostr.read(cx).relay_list_state.configured(), |this| {
|
.when(nostr.read(cx).relay_list_state.configured(), |this| {
|
||||||
this.indicator()
|
this.indicator()
|
||||||
})
|
})
|
||||||
.dropdown_menu(move |this, _window, cx| {
|
.dropdown_menu(move |this, _window, cx| {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let urls: Vec<SharedString> = vec![];
|
let urls: Vec<SharedString> = vec![];
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
let menu = this.min_w(px(260.)).label("Relays");
|
let menu = this.min_w(px(260.)).label("Relays");
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
let menu = urls.into_iter().fold(menu, |this, url| {
|
let menu = urls.into_iter().fold(menu, |this, url| {
|
||||||
this.item(PopupMenuItem::element(move |_window, _cx| {
|
this.item(PopupMenuItem::element(move |_window, _cx| {
|
||||||
h_flex()
|
h_flex()
|
||||||
.px_1()
|
.px_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(
|
.child(
|
||||||
div().size_1p5().rounded_full().bg(gpui::green()),
|
div().size_1p5().rounded_full().bg(gpui::green()),
|
||||||
)
|
)
|
||||||
.child(url.clone())
|
.child(url.clone())
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
menu.separator()
|
menu.separator()
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
"Reload",
|
"Reload",
|
||||||
IconName::Refresh,
|
IconName::Refresh,
|
||||||
Box::new(Command::RefreshRelayList),
|
Box::new(Command::RefreshRelayList),
|
||||||
)
|
)
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
"Update relay list",
|
"Update relay list",
|
||||||
IconName::Settings,
|
IconName::Settings,
|
||||||
Box::new(Command::ShowRelayList),
|
Box::new(Command::ShowRelayList),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
) */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,12 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
|
App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
|
||||||
Subscription, Task, Window, div,
|
SharedString, Styled, Task, Window, div,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use state::{Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, TIMEOUT, app_name};
|
||||||
use state::{
|
|
||||||
Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, RelayState, TIMEOUT, app_name,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -32,6 +29,15 @@ struct GlobalDeviceRegistry(Entity<DeviceRegistry>);
|
|||||||
|
|
||||||
impl Global for GlobalDeviceRegistry {}
|
impl Global for GlobalDeviceRegistry {}
|
||||||
|
|
||||||
|
/// Device event.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum DeviceEvent {
|
||||||
|
/// A new encryption signer has been set
|
||||||
|
Set,
|
||||||
|
/// An error occurred
|
||||||
|
Error(SharedString),
|
||||||
|
}
|
||||||
|
|
||||||
/// Device Registry
|
/// Device Registry
|
||||||
///
|
///
|
||||||
/// 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
|
||||||
@@ -42,11 +48,10 @@ pub struct DeviceRegistry {
|
|||||||
|
|
||||||
/// Async tasks
|
/// Async tasks
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
/// Subscriptions
|
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DeviceEvent> for DeviceRegistry {}
|
||||||
|
|
||||||
impl DeviceRegistry {
|
impl DeviceRegistry {
|
||||||
/// Retrieve the global device registry state
|
/// Retrieve the global device registry state
|
||||||
pub fn global(cx: &App) -> Entity<Self> {
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
@@ -60,27 +65,16 @@ 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 nostr = NostrRegistry::global(cx);
|
let state = DeviceState::default();
|
||||||
let mut subscriptions = smallvec![];
|
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Observe the NIP-65 state
|
|
||||||
cx.observe(&nostr, |this, state, cx| {
|
|
||||||
if state.read(cx).relay_list_state == RelayState::Configured {
|
|
||||||
this.get_announcement(cx);
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Run at the end of current cycle
|
|
||||||
cx.defer_in(window, |this, window, cx| {
|
cx.defer_in(window, |this, window, cx| {
|
||||||
this.handle_notifications(window, cx);
|
this.handle_notifications(window, cx);
|
||||||
|
this.get_announcement(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
state: DeviceState::default(),
|
state,
|
||||||
tasks: vec![],
|
tasks: vec![],
|
||||||
_subscriptions: subscriptions,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,30 +117,27 @@ impl DeviceRegistry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.tasks.push(
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
// Update GPUI states
|
while let Ok(event) = rx.recv_async().await {
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
match event.kind {
|
||||||
while let Ok(event) = rx.recv_async().await {
|
// New request event
|
||||||
match event.kind {
|
Kind::Custom(4454) => {
|
||||||
// New request event
|
this.update_in(cx, |this, window, cx| {
|
||||||
Kind::Custom(4454) => {
|
this.ask_for_approval(event, window, cx);
|
||||||
this.update_in(cx, |this, window, cx| {
|
})?;
|
||||||
this.ask_for_approval(event, window, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
// New response event
|
|
||||||
Kind::Custom(4455) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.extract_encryption(event, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
// New response event
|
||||||
|
Kind::Custom(4455) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.extract_encryption(event, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the device state
|
/// Get the device state
|
||||||
@@ -191,45 +182,68 @@ impl DeviceRegistry {
|
|||||||
fn get_messages(&mut self, cx: &mut Context<Self>) {
|
fn get_messages(&mut self, cx: &mut Context<Self>) {
|
||||||
let task = self.subscribe_to_giftwrap_events(cx);
|
let task = self.subscribe_to_giftwrap_events(cx);
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |_this, _cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
task.await?;
|
if let Err(e) = task.await {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
// Update state
|
cx.emit(DeviceEvent::Error(SharedString::from(e.to_string())));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Continuously get gift wrap events for the current user in their messaging relays
|
/// Get the messaging relays for the current user
|
||||||
fn subscribe_to_giftwrap_events(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
|
fn get_user_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, 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 signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
let Some(public_key) = signer.public_key() else {
|
cx.background_spawn(async move {
|
||||||
return Task::ready(Err(anyhow!("User not found")));
|
let public_key = signer.get_public_key().await?;
|
||||||
};
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
let persons = PersonRegistry::global(cx);
|
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||||
let profile = persons.read(cx).get(&public_key, cx);
|
// Extract relay URLs from the event
|
||||||
let relay_urls = profile.messaging_relays().clone();
|
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
||||||
|
|
||||||
|
// Ensure all relays are connected
|
||||||
|
for url in urls.iter() {
|
||||||
|
client.add_relay(url).and_connect().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(urls)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Relays not found"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Continuously get gift wrap events for the current user in their messaging relays
|
||||||
|
fn subscribe_to_giftwrap_events(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
|
let urls = self.get_user_messaging_relays(cx);
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
|
let urls = urls.await?;
|
||||||
|
let encryption = signer.get_encryption_signer().await.context("not found")?;
|
||||||
|
let public_key = encryption.get_public_key().await?;
|
||||||
|
|
||||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
let id = SubscriptionId::new(DEVICE_GIFTWRAP);
|
let id = SubscriptionId::new(DEVICE_GIFTWRAP);
|
||||||
|
|
||||||
// Construct target for subscription
|
// Construct target for subscription
|
||||||
let target: HashMap<RelayUrl, Filter> = relay_urls
|
let target: HashMap<RelayUrl, Filter> = urls
|
||||||
.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?;
|
// Subscribe
|
||||||
|
client.subscribe(target).with_id(id).await?;
|
||||||
log::info!(
|
|
||||||
"Successfully subscribed to encryption gift-wrap messages on: {:?}",
|
|
||||||
output.success
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -239,16 +253,14 @@ 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(public_key) = signer.public_key() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reset state before fetching announcement
|
// Reset state before fetching announcement
|
||||||
self.reset(cx);
|
self.reset(cx);
|
||||||
|
|
||||||
let task: Task<Result<Event, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Event, Error>> = cx.background_spawn(async move {
|
||||||
|
let signer = client.signer().context("Signer not found")?;
|
||||||
|
let public_key = signer.get_public_key().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()
|
||||||
.kind(Kind::Custom(10044))
|
.kind(Kind::Custom(10044))
|
||||||
@@ -262,18 +274,12 @@ impl DeviceRegistry {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
while let Some((_url, res)) = stream.next().await {
|
while let Some((_url, res)) = stream.next().await {
|
||||||
match res {
|
if let Ok(event) = res {
|
||||||
Ok(event) => {
|
return Ok(event);
|
||||||
log::info!("Received device announcement event: {event:?}");
|
|
||||||
return Ok(event);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to receive device announcement event: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow!("Device announcement not found"))
|
Err(anyhow!("Announcement not found"))
|
||||||
});
|
});
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
@@ -436,7 +442,7 @@ impl DeviceRegistry {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
let app_keys = nostr.read(cx).app_keys.clone();
|
let app_keys = nostr.read(cx).keys();
|
||||||
let app_pubkey = app_keys.public_key();
|
let app_pubkey = app_keys.public_key();
|
||||||
|
|
||||||
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
|
||||||
@@ -506,7 +512,7 @@ impl DeviceRegistry {
|
|||||||
/// Parse the response event for device keys from other devices
|
/// Parse the response event for device keys from other devices
|
||||||
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let app_keys = nostr.read(cx).app_keys.clone();
|
let app_keys = nostr.read(cx).keys();
|
||||||
|
|
||||||
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
|
||||||
let root_device = event
|
let root_device = event
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ smallvec.workspace = true
|
|||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{Error, anyhow};
|
||||||
use common::EventUtils;
|
use common::EventUtils;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
|
use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
|
||||||
|
|
||||||
mod person;
|
mod person;
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ pub struct PersonRegistry {
|
|||||||
persons: HashMap<PublicKey, Entity<Person>>,
|
persons: HashMap<PublicKey, Entity<Person>>,
|
||||||
|
|
||||||
/// Set of public keys that have been seen
|
/// Set of public keys that have been seen
|
||||||
seen: Rc<RefCell<HashSet<PublicKey>>>,
|
seens: Rc<RefCell<HashSet<PublicKey>>>,
|
||||||
|
|
||||||
/// Sender for requesting metadata
|
/// Sender for requesting metadata
|
||||||
sender: flume::Sender<PublicKey>,
|
sender: flume::Sender<PublicKey>,
|
||||||
@@ -63,7 +63,7 @@ impl PersonRegistry {
|
|||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
// Channel for communication between nostr and gpui
|
||||||
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
||||||
let (mta_tx, mta_rx) = flume::bounded::<PublicKey>(100);
|
let (mta_tx, mta_rx) = flume::unbounded::<PublicKey>();
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ impl PersonRegistry {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
persons: HashMap::new(),
|
persons: HashMap::new(),
|
||||||
seen: Rc::new(RefCell::new(HashSet::new())),
|
seens: Rc::new(RefCell::new(HashSet::new())),
|
||||||
sender: mta_tx,
|
sender: mta_tx,
|
||||||
_tasks: tasks,
|
_tasks: tasks,
|
||||||
}
|
}
|
||||||
@@ -208,7 +208,9 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
get_metadata(client, std::mem::take(&mut batch)).await.ok();
|
if !batch.is_empty() {
|
||||||
|
get_metadata(client, std::mem::take(&mut batch)).await.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,32 +218,42 @@ impl PersonRegistry {
|
|||||||
|
|
||||||
/// Set profile encryption keys announcement
|
/// Set profile encryption keys announcement
|
||||||
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
|
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
|
||||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
let announcement = Announcement::from(event);
|
||||||
let announcement = Announcement::from(event);
|
|
||||||
|
|
||||||
|
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||||
person.update(cx, |person, cx| {
|
person.update(cx, |person, cx| {
|
||||||
person.set_announcement(announcement);
|
person.set_announcement(announcement);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
let person =
|
||||||
|
Person::new(event.pubkey, Metadata::default()).with_announcement(announcement);
|
||||||
|
self.insert(person, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set messaging relays for a person
|
/// Set messaging relays for a person
|
||||||
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) {
|
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) {
|
||||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).cloned().collect();
|
||||||
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).cloned().collect();
|
|
||||||
|
|
||||||
|
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||||
person.update(cx, |person, cx| {
|
person.update(cx, |person, cx| {
|
||||||
person.set_messaging_relays(urls);
|
person.set_messaging_relays(urls);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
let person = Person::new(event.pubkey, Metadata::default()).with_messaging_relays(urls);
|
||||||
|
self.insert(person, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert batch of persons
|
/// Insert batch of persons
|
||||||
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
||||||
for person in persons.into_iter() {
|
for person in persons.into_iter() {
|
||||||
self.persons.insert(person.public_key(), cx.new(|_| person));
|
let public_key = person.public_key();
|
||||||
|
self.persons
|
||||||
|
.entry(public_key)
|
||||||
|
.or_insert_with(|| cx.new(|_| person));
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -270,7 +282,7 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let public_key = *public_key;
|
let public_key = *public_key;
|
||||||
let mut seen = self.seen.borrow_mut();
|
let mut seen = self.seens.borrow_mut();
|
||||||
|
|
||||||
if seen.insert(public_key) {
|
if seen.insert(public_key) {
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
|
|||||||
@@ -65,6 +65,21 @@ impl Person {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build profile encryption keys announcement
|
||||||
|
pub fn with_announcement(mut self, announcement: Announcement) -> Self {
|
||||||
|
self.announcement = Some(announcement);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build profile messaging relays
|
||||||
|
pub fn with_messaging_relays<I>(mut self, relays: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = RelayUrl>,
|
||||||
|
{
|
||||||
|
self.messaging_relays = relays.into_iter().collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Get profile public key
|
/// Get profile public key
|
||||||
pub fn public_key(&self) -> PublicKey {
|
pub fn public_key(&self) -> PublicKey {
|
||||||
self.public_key
|
self.public_key
|
||||||
@@ -75,21 +90,11 @@ impl Person {
|
|||||||
self.metadata.clone()
|
self.metadata.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set profile metadata
|
|
||||||
pub fn set_metadata(&mut self, metadata: Metadata) {
|
|
||||||
self.metadata = metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get profile encryption keys announcement
|
/// Get profile encryption keys announcement
|
||||||
pub fn announcement(&self) -> Option<Announcement> {
|
pub fn announcement(&self) -> Option<Announcement> {
|
||||||
self.announcement.clone()
|
self.announcement.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set profile encryption keys announcement
|
|
||||||
pub fn set_announcement(&mut self, announcement: Announcement) {
|
|
||||||
self.announcement = Some(announcement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get profile messaging relays
|
/// Get profile messaging relays
|
||||||
pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
|
pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
|
||||||
&self.messaging_relays
|
&self.messaging_relays
|
||||||
@@ -100,14 +105,6 @@ impl Person {
|
|||||||
self.messaging_relays.first().cloned()
|
self.messaging_relays.first().cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set profile messaging relays
|
|
||||||
pub fn set_messaging_relays<I>(&mut self, relays: I)
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = RelayUrl>,
|
|
||||||
{
|
|
||||||
self.messaging_relays = relays.into_iter().collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get profile avatar
|
/// Get profile avatar
|
||||||
pub fn avatar(&self) -> SharedString {
|
pub fn avatar(&self) -> SharedString {
|
||||||
self.metadata()
|
self.metadata()
|
||||||
@@ -115,8 +112,9 @@ impl Person {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|picture| !picture.is_empty())
|
.filter(|picture| !picture.is_empty())
|
||||||
.map(|picture| {
|
.map(|picture| {
|
||||||
|
let encoded_picture = urlencoding::encode(picture);
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{IMAGE_RESIZER}/?url={picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
|
"{IMAGE_RESIZER}/?url={encoded_picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
|
||||||
);
|
);
|
||||||
url.into()
|
url.into()
|
||||||
})
|
})
|
||||||
@@ -139,6 +137,24 @@ impl Person {
|
|||||||
|
|
||||||
SharedString::from(shorten_pubkey(self.public_key(), 4))
|
SharedString::from(shorten_pubkey(self.public_key(), 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set profile metadata
|
||||||
|
pub fn set_metadata(&mut self, metadata: Metadata) {
|
||||||
|
self.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set profile encryption keys announcement
|
||||||
|
pub fn set_announcement(&mut self, announcement: Announcement) {
|
||||||
|
self.announcement = Some(announcement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set profile messaging relays
|
||||||
|
pub fn set_messaging_relays<I>(&mut self, relays: I)
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = RelayUrl>,
|
||||||
|
{
|
||||||
|
self.messaging_relays = relays.into_iter().collect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorten a [`PublicKey`] to a string with the first and last `len` characters
|
/// Shorten a [`PublicKey`] to a string with the first and last `len` characters
|
||||||
@@ -148,7 +164,7 @@ pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String {
|
|||||||
let Ok(pubkey) = public_key.to_bech32();
|
let Ok(pubkey) = public_key.to_bech32();
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"{}:{}",
|
"{}...{}",
|
||||||
&pubkey[0..(len + 1)],
|
&pubkey[0..(len + 1)],
|
||||||
&pubkey[pubkey.len() - len..]
|
&pubkey[pubkey.len() - len..]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ use std::hash::Hash;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
|
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
|
||||||
Task, Window,
|
Task, Window,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::{AppSettings, AuthMode};
|
use settings::{AppSettings, AuthMode};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension};
|
use ui::{Disableable, IconName, Sizable, WindowExtension, v_flex};
|
||||||
|
|
||||||
const AUTH_MESSAGE: &str =
|
const AUTH_MESSAGE: &str =
|
||||||
"Approve the authentication request to allow Coop to continue sending or receiving events.";
|
"Approve the authentication request to allow Coop to continue sending or receiving events.";
|
||||||
@@ -34,7 +34,10 @@ struct AuthRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AuthRequest {
|
impl AuthRequest {
|
||||||
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
|
pub fn new<S>(challenge: S, url: RelayUrl) -> Self
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
Self {
|
Self {
|
||||||
challenge: challenge.into(),
|
challenge: challenge.into(),
|
||||||
url,
|
url,
|
||||||
@@ -106,22 +109,6 @@ impl RelayAuth {
|
|||||||
tx.send_async(signal).await.ok();
|
tx.send_async(signal).await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RelayMessage::Closed {
|
|
||||||
subscription_id,
|
|
||||||
message,
|
|
||||||
} => {
|
|
||||||
let msg = MachineReadablePrefix::parse(&message);
|
|
||||||
|
|
||||||
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
|
||||||
if let Ok(Some(relay)) = client.relay(&relay_url).await {
|
|
||||||
// Send close message to relay
|
|
||||||
relay
|
|
||||||
.send_msg(ClientMessage::Close(subscription_id))
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RelayMessage::Ok {
|
RelayMessage::Ok {
|
||||||
event_id, message, ..
|
event_id, message, ..
|
||||||
} => {
|
} => {
|
||||||
@@ -330,7 +317,7 @@ impl RelayAuth {
|
|||||||
Notification::new()
|
Notification::new()
|
||||||
.custom_id(SharedString::from(&req.challenge))
|
.custom_id(SharedString::from(&req.challenge))
|
||||||
.autohide(false)
|
.autohide(false)
|
||||||
.icon(IconName::Info)
|
.icon(IconName::Warning)
|
||||||
.title(SharedString::from("Authentication Required"))
|
.title(SharedString::from("Authentication Required"))
|
||||||
.content(move |_window, cx| {
|
.content(move |_window, cx| {
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ common = { path = "../common" }
|
|||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr-lmdb.workspace = true
|
nostr-lmdb.workspace = true
|
||||||
|
nostr-memory.workspace = true
|
||||||
nostr-gossip-sqlite.workspace = true
|
nostr-gossip-sqlite.workspace = true
|
||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
nostr-blossom.workspace = true
|
nostr-blossom.workspace = true
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use common::config_dir;
|
use common::config_dir;
|
||||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window};
|
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use nostr_gossip_sqlite::prelude::*;
|
use nostr_gossip_sqlite::prelude::*;
|
||||||
use nostr_lmdb::prelude::*;
|
use nostr_lmdb::prelude::*;
|
||||||
|
use nostr_memory::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
mod blossom;
|
mod blossom;
|
||||||
@@ -42,6 +43,21 @@ struct GlobalNostrRegistry(Entity<NostrRegistry>);
|
|||||||
|
|
||||||
impl Global for GlobalNostrRegistry {}
|
impl Global for GlobalNostrRegistry {}
|
||||||
|
|
||||||
|
/// Signer event.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum StateEvent {
|
||||||
|
/// Connecting to the bootstrapping relay
|
||||||
|
Connecting,
|
||||||
|
/// Connected to the bootstrapping relay
|
||||||
|
Connected,
|
||||||
|
/// User has not set up NIP-65 relays
|
||||||
|
RelayNotConfigured,
|
||||||
|
/// A new signer has been set
|
||||||
|
SignerSet,
|
||||||
|
/// An error occurred
|
||||||
|
Error(SharedString),
|
||||||
|
}
|
||||||
|
|
||||||
/// Nostr Registry
|
/// Nostr Registry
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NostrRegistry {
|
pub struct NostrRegistry {
|
||||||
@@ -57,15 +73,14 @@ pub struct NostrRegistry {
|
|||||||
/// App keys
|
/// App keys
|
||||||
///
|
///
|
||||||
/// Used for Nostr Connect and NIP-4e operations
|
/// Used for Nostr Connect and NIP-4e operations
|
||||||
pub app_keys: Keys,
|
app_keys: Keys,
|
||||||
|
|
||||||
/// Relay list state
|
|
||||||
pub relay_list_state: RelayState,
|
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<StateEvent> for NostrRegistry {}
|
||||||
|
|
||||||
impl NostrRegistry {
|
impl NostrRegistry {
|
||||||
/// Retrieve the global nostr state
|
/// Retrieve the global nostr state
|
||||||
pub fn global(cx: &App) -> Entity<Self> {
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
@@ -93,25 +108,32 @@ impl NostrRegistry {
|
|||||||
.expect("Failed to initialize gossip instance")
|
.expect("Failed to initialize gossip instance")
|
||||||
});
|
});
|
||||||
|
|
||||||
// Construct the nostr lmdb instance
|
// Construct the nostr client builder
|
||||||
let lmdb = cx.foreground_executor().block_on(async move {
|
let mut builder = ClientBuilder::default()
|
||||||
NostrLmdb::open(config_dir().join("nostr"))
|
|
||||||
.await
|
|
||||||
.expect("Failed to initialize database")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Construct the nostr client
|
|
||||||
let client = ClientBuilder::default()
|
|
||||||
.signer(signer.clone())
|
.signer(signer.clone())
|
||||||
.gossip(gossip)
|
.gossip(gossip)
|
||||||
.database(lmdb)
|
|
||||||
.automatic_authentication(false)
|
.automatic_authentication(false)
|
||||||
.verify_subscriptions(false)
|
.verify_subscriptions(false)
|
||||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
.connect_timeout(Duration::from_secs(TIMEOUT))
|
||||||
.sleep_when_idle(SleepWhenIdle::Enabled {
|
.sleep_when_idle(SleepWhenIdle::Enabled {
|
||||||
timeout: Duration::from_secs(600),
|
timeout: Duration::from_secs(600),
|
||||||
})
|
});
|
||||||
.build();
|
|
||||||
|
// Add database if not in debug mode
|
||||||
|
if !cfg!(debug_assertions) {
|
||||||
|
// Construct the nostr lmdb instance
|
||||||
|
let lmdb = cx.foreground_executor().block_on(async move {
|
||||||
|
NostrLmdb::open(config_dir().join("nostr"))
|
||||||
|
.await
|
||||||
|
.expect("Failed to initialize database")
|
||||||
|
});
|
||||||
|
builder = builder.database(lmdb);
|
||||||
|
} else {
|
||||||
|
builder = builder.database(MemoryDatabase::unbounded())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the nostr client
|
||||||
|
let client = builder.build();
|
||||||
|
|
||||||
// Run at the end of current cycle
|
// Run at the end of current cycle
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
@@ -123,7 +145,6 @@ impl NostrRegistry {
|
|||||||
signer,
|
signer,
|
||||||
npubs,
|
npubs,
|
||||||
app_keys,
|
app_keys,
|
||||||
relay_list_state: RelayState::Idle,
|
|
||||||
tasks: vec![],
|
tasks: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,10 +164,18 @@ impl NostrRegistry {
|
|||||||
self.npubs.clone()
|
self.npubs.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the app keys
|
||||||
|
pub fn keys(&self) -> Keys {
|
||||||
|
self.app_keys.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Connect to the bootstrapping relays
|
/// Connect to the bootstrapping relays
|
||||||
fn connect(&mut self, cx: &mut Context<Self>) {
|
fn connect(&mut self, cx: &mut Context<Self>) {
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
|
|
||||||
|
// 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| {
|
||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.await_on_background(async move {
|
.await_on_background(async move {
|
||||||
@@ -167,6 +196,7 @@ impl NostrRegistry {
|
|||||||
|
|
||||||
// Update the state
|
// Update the state
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
cx.emit(StateEvent::Connected);
|
||||||
this.get_npubs(cx);
|
this.get_npubs(cx);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -177,6 +207,7 @@ impl NostrRegistry {
|
|||||||
/// Get all used npubs
|
/// Get all used npubs
|
||||||
fn get_npubs(&mut self, cx: &mut Context<Self>) {
|
fn get_npubs(&mut self, cx: &mut Context<Self>) {
|
||||||
let npubs = self.npubs.downgrade();
|
let npubs = self.npubs.downgrade();
|
||||||
|
|
||||||
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
||||||
let dir = config_dir().join("keys");
|
let dir = config_dir().join("keys");
|
||||||
// Ensure keys directory exists
|
// Ensure keys directory exists
|
||||||
@@ -227,9 +258,8 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to get npubs: {e}");
|
this.update(cx, |_this, cx| {
|
||||||
this.update(cx, |this, cx| {
|
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||||
this.create_identity(cx);
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,14 +431,13 @@ impl NostrRegistry {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit signer changed event
|
// Emit signer changed event
|
||||||
cx.emit(SignerEvent::Set);
|
cx.emit(StateEvent::SignerSet);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(SignerEvent::Error(e.to_string()));
|
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -456,7 +485,7 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(SignerEvent::Error(e.to_string()));
|
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,14 +524,14 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(SignerEvent::Error(e.to_string()));
|
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(SignerEvent::Error(e.to_string()));
|
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -666,8 +695,6 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<SignerEvent> for NostrRegistry {}
|
|
||||||
|
|
||||||
/// Get or create a new app keys
|
/// Get or create a new app keys
|
||||||
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
||||||
let dir = config_dir().join(".app_keys");
|
let dir = config_dir().join(".app_keys");
|
||||||
@@ -726,43 +753,6 @@ fn default_messaging_relays() -> Vec<RelayUrl> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signer event.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum SignerEvent {
|
|
||||||
/// A new signer has been set
|
|
||||||
Set,
|
|
||||||
|
|
||||||
/// An error occurred
|
|
||||||
Error(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
||||||
pub enum RelayState {
|
|
||||||
#[default]
|
|
||||||
Idle,
|
|
||||||
Checking,
|
|
||||||
NotConfigured,
|
|
||||||
Configured,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RelayState {
|
|
||||||
pub fn idle(&self) -> bool {
|
|
||||||
matches!(self, RelayState::Idle)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn checking(&self) -> bool {
|
|
||||||
matches!(self, RelayState::Checking)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn not_configured(&self) -> bool {
|
|
||||||
matches!(self, RelayState::NotConfigured)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn configured(&self) -> bool {
|
|
||||||
matches!(self, RelayState::Configured)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CoopAuthUrlHandler;
|
pub struct CoopAuthUrlHandler;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user