Redesign for the v1 stable release (#3)
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m26s

Only half done. Will continue in another PR.

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-02-04 01:43:21 +00:00
parent 014757cfc9
commit 32201554ec
174 changed files with 6165 additions and 8112 deletions

View File

@@ -16,7 +16,7 @@ use gpui::{
};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use state::{tracker, NostrRegistry, GIFTWRAP_SUBSCRIPTION};
use state::{tracker, NostrRegistry, RelayState, DEVICE_GIFTWRAP, USER_GIFTWRAP};
mod message;
mod room;
@@ -63,14 +63,17 @@ pub struct ChatRegistry {
/// Loading status of the registry
loading: bool,
/// Tracking the status of unwrapping gift wrap events.
tracking_flag: Arc<AtomicBool>,
/// Channel's sender for communication between nostr and gpui
sender: Sender<NostrEvent>,
/// Tracking the status of unwrapping gift wrap events.
tracking_flag: Arc<AtomicBool>,
/// Handle tracking asynchronous task
tracking: Option<Task<Result<(), Error>>>,
/// Handle notifications asynchronous task
notifications: Option<Task<Result<(), Error>>>,
notifications: Option<Task<()>>,
/// Tasks for asynchronous operations
tasks: Vec<Task<()>>,
@@ -101,7 +104,7 @@ impl ChatRegistry {
let device_signer = device.read(cx).device_signer.clone();
// A flag to indicate if the registry is loading
let tracking_flag = Arc::new(AtomicBool::new(true));
let tracking_flag = Arc::new(AtomicBool::new(false));
// Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<NostrEvent>(2048);
@@ -112,12 +115,14 @@ impl ChatRegistry {
subscriptions.push(
// Observe the identity
cx.observe(&identity, |this, state, cx| {
if state.read(cx).has_public_key() {
if state.read(cx).messaging_relays_state() == RelayState::Set {
// Handle nostr notifications
this.handle_notifications(cx);
// Track unwrapping progress
this.tracking(cx);
}
// Get chat rooms from the database on every identity change
this.get_rooms(cx);
}),
);
@@ -161,9 +166,10 @@ impl ChatRegistry {
Self {
rooms: vec![],
loading: true,
tracking_flag,
loading: false,
sender: tx.clone(),
tracking_flag,
tracking: None,
notifications: None,
tasks,
_subscriptions: subscriptions,
@@ -181,9 +187,10 @@ impl ChatRegistry {
let status = self.tracking_flag.clone();
let tx = self.sender.clone();
self.tasks.push(cx.background_spawn(async move {
self.notifications = Some(cx.background_spawn(async move {
let initialized_at = Timestamp::now();
let subscription_id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
let sub_id1 = SubscriptionId::new(DEVICE_GIFTWRAP);
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
let mut notifications = client.notifications();
let mut processed_events = HashSet::new();
@@ -229,12 +236,12 @@ impl ChatRegistry {
}
},
Err(e) => {
log::warn!("Failed to unwrap: {e}");
log::warn!("Failed to unwrap the gift wrap event: {e}");
}
}
}
RelayMessage::EndOfStoredEvents(id) => {
if id.as_ref() == &subscription_id {
if id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2 {
tx.send_async(NostrEvent::Eose).await.ok();
}
}
@@ -246,44 +253,18 @@ impl ChatRegistry {
/// Tracking the status of unwrapping gift wrap events.
fn tracking(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let status = self.tracking_flag.clone();
let tx = self.sender.clone();
self.notifications = Some(cx.background_spawn(async move {
self.tracking = Some(cx.background_spawn(async move {
let loop_duration = Duration::from_secs(12);
let mut is_start_processing = false;
let mut total_loops = 0;
loop {
if client.has_signer().await {
total_loops += 1;
if status.load(Ordering::Acquire) {
is_start_processing = true;
// Reset gift wrap processing flag
_ = status.compare_exchange(
true,
false,
Ordering::Release,
Ordering::Relaxed,
);
tx.send_async(NostrEvent::Unwrapping(true)).await.ok();
} else {
// Only run further if we are already processing
// Wait until after 2 loops to prevent exiting early while events are still being processed
if is_start_processing && total_loops >= 2 {
tx.send_async(NostrEvent::Unwrapping(false)).await.ok();
// Reset the counter
is_start_processing = false;
total_loops = 0;
}
}
if status.load(Ordering::Acquire) {
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
tx.send_async(NostrEvent::Unwrapping(true)).await.ok();
} else {
tx.send_async(NostrEvent::Unwrapping(false)).await.ok();
}
smol::Timer::after(loop_duration).await;
}
@@ -309,22 +290,21 @@ impl ChatRegistry {
.map(|this| this.downgrade())
}
/// Get all ongoing rooms.
pub fn ongoing_rooms(&self, cx: &App) -> Vec<Entity<Room>> {
/// Get all rooms based on the filter.
pub fn rooms(&self, filter: &RoomKind, cx: &App) -> Vec<Entity<Room>> {
self.rooms
.iter()
.filter(|room| room.read(cx).kind == RoomKind::Ongoing)
.filter(|room| &room.read(cx).kind == filter)
.cloned()
.collect()
}
/// Get all request rooms.
pub fn request_rooms(&self, cx: &App) -> Vec<Entity<Room>> {
/// Count the number of rooms based on the filter.
pub fn count(&self, filter: &RoomKind, cx: &App) -> usize {
self.rooms
.iter()
.filter(|room| room.read(cx).kind != RoomKind::Ongoing)
.cloned()
.collect()
.filter(|room| &room.read(cx).kind == filter)
.count()
}
/// Add a new room to the start of list.
@@ -337,6 +317,7 @@ impl ChatRegistry {
}
/// Emit an open room event.
///
/// If the room is new, add it to the registry.
pub fn emit_room(&mut self, room: WeakEntity<Room>, cx: &mut Context<Self>) {
if let Some(room) = room.upgrade() {
@@ -365,28 +346,27 @@ impl ChatRegistry {
cx.notify();
}
/// Search rooms by their name.
pub fn search(&self, query: &str, cx: &App) -> Vec<Entity<Room>> {
/// Finding rooms based on a query.
pub fn find(&self, query: &str, cx: &App) -> Vec<Entity<Room>> {
let matcher = SkimMatcherV2::default();
self.rooms
.iter()
.filter(|room| {
matcher
.fuzzy_match(room.read(cx).display_name(cx).as_ref(), query)
.is_some()
})
.cloned()
.collect()
}
/// Search rooms by public keys.
pub fn search_by_public_key(&self, public_key: PublicKey, cx: &App) -> Vec<Entity<Room>> {
self.rooms
.iter()
.filter(|room| room.read(cx).members.contains(&public_key))
.cloned()
.collect()
if let Ok(public_key) = PublicKey::parse(query) {
self.rooms
.iter()
.filter(|room| room.read(cx).members.contains(&public_key))
.cloned()
.collect()
} else {
self.rooms
.iter()
.filter(|room| {
matcher
.fuzzy_match(room.read(cx).display_name(cx).as_ref(), query)
.is_some()
})
.cloned()
.collect()
}
}
/// Reset the registry.
@@ -532,7 +512,7 @@ impl ChatRegistry {
}
}
/// Parse a Nostr event into a Coop Message and push it to the belonging room
/// Parse a nostr event into a message and push it to the belonging room
///
/// If the room doesn't exist, it will be created.
/// Updates room ordering based on the most recent messages.
@@ -579,7 +559,7 @@ impl ChatRegistry {
}
}
// Unwraps a gift-wrapped event and processes its contents.
/// Unwraps a gift-wrapped event and processes its contents.
async fn extract_rumor(
client: &Client,
device_signer: &Option<Arc<dyn NostrSigner>>,
@@ -603,35 +583,50 @@ impl ChatRegistry {
Ok(rumor_unsigned)
}
// Helper method to try unwrapping with different signers
/// Helper method to try unwrapping with different signers
async fn try_unwrap(
client: &Client,
device_signer: &Option<Arc<dyn NostrSigner>>,
gift_wrap: &Event,
) -> Result<UnwrappedGift, Error> {
if let Some(signer) = device_signer.as_ref() {
let seal = signer
.nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content)
.await?;
// Try with the device signer first
if let Some(signer) = device_signer {
if let Ok(unwrapped) = Self::try_unwrap_with(gift_wrap, signer).await {
return Ok(unwrapped);
};
};
let seal: Event = Event::from_json(seal)?;
seal.verify_with_ctx(&SECP256K1)?;
let rumor = signer.nip44_decrypt(&seal.pubkey, &seal.content).await?;
let rumor = UnsignedEvent::from_json(rumor)?;
return Ok(UnwrappedGift {
sender: seal.pubkey,
rumor,
});
}
let signer = client.signer().await?;
let unwrapped = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?;
// Try with the user's signer
let user_signer = client.signer().await?;
let unwrapped = UnwrappedGift::from_gift_wrap(&user_signer, gift_wrap).await?;
Ok(unwrapped)
}
/// Attempts to unwrap a gift wrap event with a given signer.
async fn try_unwrap_with(
gift_wrap: &Event,
signer: &Arc<dyn NostrSigner>,
) -> Result<UnwrappedGift, Error> {
// Get the sealed event
let seal = signer
.nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content)
.await?;
// Verify the sealed event
let seal: Event = Event::from_json(seal)?;
seal.verify_with_ctx(&SECP256K1)?;
// Get the rumor event
let rumor = signer.nip44_decrypt(&seal.pubkey, &seal.content).await?;
let rumor = UnsignedEvent::from_json(rumor)?;
Ok(UnwrappedGift {
sender: seal.pubkey,
rumor,
})
}
/// Stores an unwrapped event in local database with reference to original
async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
let rumor_id = rumor.id.context("Rumor is missing an event id")?;

View File

@@ -167,22 +167,11 @@ impl From<&UnsignedEvent> for Room {
impl Room {
/// Constructs a new room with the given receiver and tags.
pub fn new(subject: Option<String>, author: PublicKey, receivers: Vec<PublicKey>) -> Self {
// Convert receiver's public keys into tags
let mut tags: Tags = Tags::from_list(
receivers
.iter()
.map(|pubkey| Tag::public_key(pubkey.to_owned()))
.collect(),
);
// Add subject if it is present
if let Some(subject) = subject {
tags.push(Tag::from_standardized_without_cell(TagStandard::Subject(
subject,
)));
}
pub fn new<T>(author: PublicKey, receivers: T) -> Self
where
T: IntoIterator<Item = PublicKey>,
{
let tags = Tags::from_list(receivers.into_iter().map(Tag::public_key).collect());
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, "")
.tags(tags)
.build(author);