fix NIP-4e state hygiene and UX issues

This commit is contained in:
2026-10-04 13:10:15 +07:00
parent bdf9249eab
commit 0bae051316
6 changed files with 101 additions and 63 deletions
+71 -46
View File
@@ -10,7 +10,6 @@ use gpui::{
App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
SharedString, Styled, Subscription, Task, Window, div, relative,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use settings::AppSettings;
@@ -59,19 +58,14 @@ impl DeviceEvent {
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
#[derive(Debug)]
pub struct DeviceRegistry {
signer: Entity<Option<UniversalSigner>>,
/// Whether there is a pending request for encryption key approval
pub pending_request: bool,
/// Whether an announcement has been made for this device
pub announcement_existed: Arc<AtomicBool>,
/// Signer
signer: Entity<Option<UniversalSigner>>,
/// Async tasks
/// Relay notification listener
notification_listener: Option<Task<Result<(), Error>>>,
tasks: Vec<Task<Result<(), Error>>>,
/// Event subscription
_subscriptions: SmallVec<[Subscription; 2]>,
}
@@ -110,6 +104,9 @@ impl DeviceRegistry {
// Observe the user signer
cx.subscribe(&nostr, move |this, _nostr, event, cx| {
if event.signer_changed() && settings.read(cx).is_nip4e_enabled(cx) {
// The old account's state must not leak into the new one
this.announcement_existed.store(false, Ordering::Relaxed);
this.handle_notifications(cx);
this.get_announcement(cx);
}
}),
@@ -125,12 +122,15 @@ impl DeviceRegistry {
signer,
pending_request: false,
announcement_existed: Arc::new(AtomicBool::new(false)),
notification_listener: None,
tasks: vec![],
_subscriptions: subscriptions,
}
}
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
self.notification_listener = None;
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
@@ -138,7 +138,7 @@ impl DeviceRegistry {
let announcement_existed = self.announcement_existed.clone();
let (tx, rx) = flume::bounded::<Event>(100);
self.tasks.push(cx.background_spawn(async move {
self.notification_listener = Some(cx.background_spawn(async move {
let mut notifications = client.notifications();
let mut processed_events = HashSet::new();
let current_user = signer.get_public_key_async().await.ok();
@@ -245,39 +245,41 @@ impl DeviceRegistry {
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
self.tasks.push(cx.background_spawn(async move {
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
let filter = Filter::new()
.kind(Kind::Custom(10044))
.author(current_user)
.limit(1);
client
.subscribe(filter)
.close_on(opts)
.with_id(SubscriptionId::new("nip4e"))
.await?;
Ok(())
}));
let announcement_existed = self.announcement_existed.clone();
self.tasks.push(cx.spawn(async move |this, cx| {
// Wait for 5 seconds
cx.background_executor().timer(Duration::from_secs(5)).await;
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let opts =
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let current_user = signer.get_public_key_async().await?;
// Then check if the msg relays have been found
if announcement_existed.load(Ordering::Acquire) {
// Construct the filter for the device announcement event
let filter = Filter::new()
.kind(Kind::Custom(10044))
.author(current_user)
.limit(1);
client
.subscribe(filter)
.close_on(opts)
.with_id(SubscriptionId::new("nip4e"))
.await?;
Ok(())
});
if let Err(e) = task.await {
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::error(e.to_string()));
})?;
return Ok(());
}
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::NotSet);
})?;
if !announcement_existed.load(Ordering::Acquire) {
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::NotSet);
})?;
}
Ok(())
}));
@@ -340,11 +342,15 @@ impl DeviceRegistry {
/// Set encryption key from the announcement event
fn set_encryption(&mut self, event: &Event, cx: &mut Context<Self>) {
let Some(announcement) = Announcement::from_event(event) else {
log::warn!("Ignoring malformed encryption announcement");
return;
};
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let announcement = Announcement::from(event);
let device_pubkey = announcement.public_key();
// Get encryption key from the database and compare with the announcement
@@ -665,11 +671,15 @@ impl DeviceRegistry {
}
self.set_pending_request(true, cx);
// Show notification
let notification = self.notification(event, cx);
let notification = match self.notification(event, cx) {
Ok(notification) => notification,
Err(e) => {
log::warn!("Ignoring invalid encryption key request: {e}");
self.set_pending_request(false, cx);
return;
}
};
// The registry is global and not bound to a window, so surface the
// request in an open window.
if let Some(window) = cx.windows().first().copied() {
if let Err(error) = window.update(cx, |_view, window, cx| {
window.push_notification(notification, cx);
@@ -682,19 +692,34 @@ impl DeviceRegistry {
}
/// Build a notification for the encryption request.
fn notification(&self, event: Event, cx: &Context<Self>) -> Notification {
fn notification(&self, event: Event, cx: &Context<Self>) -> Result<Notification, Error> {
const MSG: &str = "You've requested an encryption key from another device. \
Approve to allow Coop to share with it.";
let request = Announcement::from(&event);
let requester = event
.tags
.iter()
.find(|tag| tag.kind() == "P")
.and_then(|tag| tag.content())
.and_then(|content| PublicKey::parse(content).ok())
.context("Request has no valid requester key")?;
let client_name = event
.tags
.iter()
.find(|tag| tag.kind() == "client")
.and_then(|tag| tag.content())
.map(|c| c.to_string());
let request = Announcement::new(requester, client_name);
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&request.public_key(), cx);
let profile = persons.read(cx).get(&requester, cx);
let entity = cx.entity().downgrade();
let loading = Rc::new(Cell::new(false));
let key = SharedString::from(event.id.to_hex());
Notification::new()
Ok(Notification::new()
.type_id::<DeviceNotification>(key)
.autohide(false)
.with_kind(NotificationKind::Info)
@@ -786,7 +811,7 @@ impl DeviceRegistry {
.ok();
}
})
})
}))
}
}