clean up
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m24s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m26s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled

This commit is contained in:
2026-02-22 16:54:51 +07:00
parent 67ccfcb132
commit 31df6d7937
13 changed files with 148 additions and 176 deletions

2
Cargo.lock generated
View File

@@ -1658,6 +1658,7 @@ dependencies = [
"itertools 0.13.0",
"log",
"nostr-sdk",
"person",
"serde",
"serde_json",
"smallvec",
@@ -4658,7 +4659,6 @@ version = "0.3.0"
dependencies = [
"anyhow",
"common",
"device",
"flume",
"gpui",
"log",

View File

@@ -50,11 +50,28 @@ enum Signal {
Eose,
}
/// 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)
}
}
/// Chat Registry
#[derive(Debug)]
pub struct ChatRegistry {
/// Relay state for messaging relay list
messaging_relay_list: Entity<RelayState>,
state: Entity<InboxState>,
/// Collection of all chat rooms
rooms: Vec<Entity<Room>>,
@@ -84,7 +101,7 @@ impl ChatRegistry {
/// Create a new chat registry instance
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let messaging_relay_list = cx.new(|_| RelayState::default());
let state = cx.new(|_| InboxState::default());
let nostr = NostrRegistry::global(cx);
let mut subscriptions = smallvec![];
@@ -106,26 +123,28 @@ impl ChatRegistry {
subscriptions.push(
// Observe the nip17 state and load chat rooms on every state change
cx.observe(&messaging_relay_list, |this, state, cx| {
match state.read(cx) {
RelayState::Configured => {
this.get_messages(cx);
}
_ => {
this.get_rooms(cx);
}
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);
}
}),
);
// Run at the end of current cycle
// Run at the end of the current cycle
cx.defer_in(window, |this, _window, cx| {
// Handle nostr notifications
this.handle_notifications(cx);
// Track unwrap gift wrap progress
this.tracking(cx);
// Load chat rooms
this.get_rooms(cx);
});
Self {
messaging_relay_list,
state,
rooms: vec![],
tracking_flag: Arc::new(AtomicBool::new(false)),
tasks: smallvec![],
@@ -170,8 +189,6 @@ impl ChatRegistry {
continue;
}
log::info!("Received gift wrap event: {:?}", event);
// Extract the rumor from the gift wrap event
match Self::extract_rumor(&client, &device_signer, event.as_ref()).await {
Ok(rumor) => match rumor.created_at >= initialized_at {
@@ -238,17 +255,19 @@ impl ChatRegistry {
}));
}
/// Ensure messaging relays are set up for the current user.
fn ensure_messaging_relays(&mut self, cx: &mut Context<Self>) {
let state = self.messaging_relay_list.downgrade();
let task = self.verify_relays(cx);
self.tasks.push(cx.spawn(async move |_this, cx| {
// Set state to checking
self.set_state(InboxState::Checking, cx);
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await?;
// Update state
state.update(cx, |this, cx| {
*this = result;
cx.notify();
this.update(cx, |this, cx| {
this.set_state(result, cx);
})?;
Ok(())
@@ -256,13 +275,12 @@ impl ChatRegistry {
}
// Verify messaging relay list for current user
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<RelayState, Error>> {
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let public_key = signer.public_key().unwrap();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
cx.background_spawn(async move {
@@ -287,8 +305,7 @@ impl ChatRegistry {
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
log::info!("Received relay list event: {event:?}");
return Ok(RelayState::Configured);
return Ok(InboxState::RelayConfigured(Box::new(event)));
}
Err(e) => {
log::error!("Failed to receive relay list event: {e}");
@@ -296,41 +313,54 @@ impl ChatRegistry {
}
}
Ok(RelayState::NotConfigured)
Ok(InboxState::RelayNotAvailable)
})
}
/// Get all messages for current user
fn get_messages(&mut self, cx: &mut Context<Self>) {
let task = self.subscribe_to_giftwrap_events(cx);
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| {
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
fn subscribe_to_giftwrap_events(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
fn subscribe<I>(&mut self, urls: I, cx: &mut Context<Self>) -> Task<Result<(), Error>>
where
I: IntoIterator<Item = RelayUrl>,
{
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let public_key = signer.public_key().unwrap();
let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx);
let urls = urls.into_iter().collect::<Vec<_>>();
cx.background_spawn(async move {
let urls = messaging_relays.await;
let public_key = signer.get_public_key().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(USER_GIFTWRAP);
// Ensure relay connections
for url in urls.iter() {
client.add_relay(url).and_connect().await?;
}
// Construct target for subscription
let target: HashMap<&RelayUrl, Filter> =
urls.iter().map(|relay| (relay, filter.clone())).collect();
let target: HashMap<RelayUrl, Filter> = urls
.into_iter()
.map(|relay| (relay, filter.clone()))
.collect();
let output = client.subscribe(target).with_id(id).await?;
@@ -343,9 +373,17 @@ 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 relay_state(&self, cx: &App) -> RelayState {
self.messaging_relay_list.read(cx).clone()
pub fn state(&self, cx: &App) -> InboxState {
self.state.read(cx).clone()
}
/// Get the loading status of the chat registry
@@ -491,16 +529,21 @@ impl ChatRegistry {
pub fn get_rooms(&mut self, cx: &mut Context<Self>) {
let task = self.get_rooms_from_database(cx);
cx.spawn(async move |this, cx| {
let rooms = task.await.ok()?;
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(rooms) => {
this.update(cx, |this, cx| {
this.extend_rooms(rooms, cx);
this.sort(cx);
})?;
}
Err(e) => {
log::error!("Failed to load rooms: {}", e);
}
};
this.update(cx, move |this, cx| {
this.extend_rooms(rooms, cx);
this.sort(cx);
})
.ok()
})
.detach();
Ok(())
}));
}
/// Create a task to load rooms from the database

View File

@@ -1216,25 +1216,24 @@ impl ChatPanel {
let encryption = matches!(signer_kind, SignerKind::Encryption);
let user = matches!(signer_kind, SignerKind::User);
this.check_side(ui::Side::Right)
.menu_with_check_and_disabled(
"Auto",
auto,
Box::new(Command::ChangeSigner(SignerKind::Auto)),
auto,
)
.menu_with_check_and_disabled(
"Decoupled Encryption Key",
encryption,
Box::new(Command::ChangeSigner(SignerKind::Encryption)),
encryption,
)
.menu_with_check_and_disabled(
"User Identity",
user,
Box::new(Command::ChangeSigner(SignerKind::User)),
user,
)
this.menu_with_check_and_disabled(
"Auto",
auto,
Box::new(Command::ChangeSigner(SignerKind::Auto)),
auto,
)
.menu_with_check_and_disabled(
"Decoupled Encryption Key",
encryption,
Box::new(Command::ChangeSigner(SignerKind::Encryption)),
encryption,
)
.menu_with_check_and_disabled(
"User Identity",
user,
Box::new(Command::ChangeSigner(SignerKind::User)),
user,
)
})
}
@@ -1339,8 +1338,8 @@ impl Render for ChatPanel {
h_flex()
.pl_1()
.gap_1()
.child(self.render_encryption_menu(window, cx))
.child(self.render_emoji_menu(window, cx))
.child(self.render_encryption_menu(window, cx))
.child(
Button::new("send")
.icon(IconName::PaperPlaneFill)

View File

@@ -1,4 +1,4 @@
use chat::ChatRegistry;
use chat::{ChatRegistry, InboxState};
use gpui::prelude::FluentBuilder;
use gpui::{
div, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
@@ -83,15 +83,16 @@ impl Render for GreeterPanel {
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
let chat = ChatRegistry::global(cx);
let nip17_state = chat.read(cx).relay_state(cx);
let nip17 = chat.read(cx).state(cx);
let nostr = NostrRegistry::global(cx);
let nip65_state = nostr.read(cx).relay_list_state();
let nip65 = nostr.read(cx).relay_list_state();
let signer = nostr.read(cx).signer();
let owned = signer.owned();
let required_actions =
nip65_state == RelayState::NotConfigured || nip17_state == RelayState::NotConfigured;
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
h_flex()
.size_full()
@@ -152,7 +153,7 @@ impl Render for GreeterPanel {
v_flex()
.gap_2()
.w_full()
.when(nip65_state.not_configured(), |this| {
.when(nip65.not_configured(), |this| {
this.child(
Button::new("relaylist")
.icon(Icon::new(IconName::Relay))
@@ -170,7 +171,7 @@ impl Render for GreeterPanel {
}),
)
})
.when(nip17_state.not_configured(), |this| {
.when(nip17.not_configured(), |this| {
this.child(
Button::new("import")
.icon(Icon::new(IconName::Relay))

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use chat::{ChatEvent, ChatRegistry};
use chat::{ChatEvent, ChatRegistry, InboxState};
use gpui::prelude::FluentBuilder;
use gpui::{
div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
@@ -233,13 +233,13 @@ impl Workspace {
),
_ => this,
})
.map(|this| match chat.read(cx).relay_state(cx) {
RelayState::Checking => {
.map(|this| match chat.read(cx).state(cx) {
InboxState::Checking => {
this.child(div().text_xs().text_color(cx.theme().text_muted).child(
SharedString::from("Fetching user's messaging relay list..."),
))
}
RelayState::NotConfigured => this.child(
InboxState::RelayNotAvailable => this.child(
h_flex()
.h_6()
.w_full()

View File

@@ -7,6 +7,7 @@ publish.workspace = true
[dependencies]
common = { path = "../common" }
state = { path = "../state" }
person = { path = "../person" }
gpui.workspace = true
nostr-sdk.workspace = true

View File

@@ -4,12 +4,11 @@ use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use state::{app_name, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT};
mod device;
pub use device::*;
use state::{
app_name, Announcement, DeviceState, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT,
};
const IDENTIFIER: &str = "coop:device";
@@ -218,16 +217,17 @@ impl DeviceRegistry {
let signer = nostr.read(cx).signer();
let public_key = signer.public_key().unwrap();
let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx);
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
let relay_urls = profile.messaging_relays().clone();
cx.background_spawn(async move {
let relay_urls = messaging_relays.await;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(DEVICE_GIFTWRAP);
// Construct target for subscription
let target: HashMap<&RelayUrl, Filter> = relay_urls
.iter()
let target: HashMap<RelayUrl, Filter> = relay_urls
.into_iter()
.map(|relay| (relay, filter.clone()))
.collect();

View File

@@ -7,7 +7,6 @@ publish.workspace = true
[dependencies]
common = { path = "../common" }
state = { path = "../state" }
device = { path = "../device" }
gpui.workspace = true
nostr-sdk.workspace = true

View File

@@ -5,11 +5,10 @@ use std::time::Duration;
use anyhow::{anyhow, Error};
use common::EventUtils;
use device::Announcement;
use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
mod person;

View File

@@ -1,9 +1,9 @@
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use device::Announcement;
use gpui::SharedString;
use nostr_sdk::prelude::*;
use state::Announcement;
const IMAGE_RESIZER: &str = "https://wsrv.nl";

View File

@@ -5,10 +5,7 @@ use nostr_sdk::prelude::*;
/// Gossip
#[derive(Debug, Clone, Default)]
pub struct Gossip {
/// Gossip relays for each public key
relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
/// Messaging relays for each public key
messaging_relays: HashMap<PublicKey, HashSet<RelayUrl>>,
}
impl Gossip {
@@ -69,39 +66,5 @@ impl Gossip {
})
.take(3),
);
log::info!("Updating gossip relays for: {}", event.pubkey);
}
/// Get messaging relays for a given public key
pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
self.messaging_relays
.get(public_key)
.cloned()
.unwrap_or_default()
.into_iter()
.collect()
}
/// Insert messaging relays for a public key
pub fn insert_messaging_relays(&mut self, event: &Event) {
self.messaging_relays
.entry(event.pubkey)
.or_default()
.extend(
event
.tags
.iter()
.filter_map(|tag| {
if let Some(TagStandard::Relay(url)) = tag.as_standardized() {
Some(url.to_owned())
} else {
None
}
})
.take(3),
);
log::info!("Updating messaging relays for: {}", event.pubkey);
}
}

View File

@@ -11,12 +11,14 @@ use nostr_lmdb::prelude::*;
use nostr_sdk::prelude::*;
mod constants;
mod device;
mod gossip;
mod nip05;
mod nip96;
mod signer;
pub use constants::*;
pub use device::*;
pub use gossip::*;
pub use nip05::*;
pub use nip96::*;
@@ -181,18 +183,16 @@ impl NostrRegistry {
} = notification
{
// Skip if the event has already been processed
if !processed_events.insert(event.id) {
continue;
}
match event.kind {
Kind::RelayList => {
tx.send_async(event.into_owned()).await?;
if processed_events.insert(event.id) {
match event.kind {
Kind::RelayList => {
tx.send_async(event.into_owned()).await?;
}
Kind::InboxRelays => {
tx.send_async(event.into_owned()).await?;
}
_ => {}
}
Kind::InboxRelays => {
tx.send_async(event.into_owned()).await?;
}
_ => {}
}
}
}
@@ -205,20 +205,11 @@ impl NostrRegistry {
self.tasks.push(cx.spawn(async move |_this, cx| {
while let Ok(event) = rx.recv_async().await {
match event.kind {
Kind::RelayList => {
gossip.update(cx, |this, cx| {
this.insert_relays(&event);
cx.notify();
})?;
}
Kind::InboxRelays => {
gossip.update(cx, |this, cx| {
this.insert_messaging_relays(&event);
cx.notify();
})?;
}
_ => {}
if let Kind::RelayList = event.kind {
gossip.update(cx, |this, cx| {
this.insert_relays(&event);
cx.notify();
})?;
}
}
@@ -256,15 +247,6 @@ impl NostrRegistry {
self.relay_list_state.clone()
}
/// Get a relay hint (messaging relay) for a given public key
pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Option<RelayUrl> {
self.gossip
.read(cx)
.messaging_relays(public_key)
.first()
.cloned()
}
/// Get a list of write relays for a given public key
pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
@@ -295,21 +277,6 @@ impl NostrRegistry {
})
}
/// Get a list of messaging relays for a given public key
pub fn messaging_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let relays = self.gossip.read(cx).messaging_relays(public_key);
cx.background_spawn(async move {
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).and_connect().await.ok();
}
relays
})
}
/// Set the connected status of the client
fn set_connected(&mut self, cx: &mut Context<Self>) {
self.connected = true;