This commit is contained in:
2026-03-09 20:21:15 +07:00
parent 39c04cabad
commit 4b4f6913bb
3 changed files with 231 additions and 315 deletions

View File

@@ -1,3 +1,5 @@
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
use ::settings::AppSettings;
@@ -21,7 +23,7 @@ use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::Notification;
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
use crate::dialogs::{accounts, settings};
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
@@ -96,9 +98,39 @@ impl Workspace {
subscriptions.push(
// Subscribe to the signer events
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
if let StateEvent::SignerSet = event {
this.set_center_layout(window, cx);
}
match event {
StateEvent::Connecting => {
let note = Notification::new()
.message("Connecting to the bootstrap relay...")
.title("Relays")
.icon(IconName::Relay);
window.push_notification(note, cx);
}
StateEvent::Connected => {
let note = Notification::new()
.message("Connected to the bootstrap relay")
.title("Relays")
.icon(IconName::Relay);
window.push_notification(note, cx);
}
StateEvent::RelayNotConfigured => {
this.relay_notification(window, cx);
}
StateEvent::RelayConnected => {
let note = Notification::new()
.message("Connected to user's relay list")
.title("Relays")
.icon(IconName::Relay);
window.push_notification(note, cx);
}
StateEvent::SignerSet => {
this.set_center_layout(window, cx);
}
_ => {}
};
}),
);
@@ -283,27 +315,16 @@ impl Workspace {
this.get_announcement(cx);
});
}
Command::RefreshRelayList => {
let nostr = NostrRegistry::global(cx);
nostr.update(cx, |this, cx| {
//this.ensure_relay_list(cx);
});
}
Command::ResetEncryption => {
self.confirm_reset_encryption(window, cx);
}
Command::RefreshMessagingRelays => {
let chat = ChatRegistry::global(cx);
chat.update(cx, |this, cx| {
//this.ensure_messaging_relays(cx);
});
}
Command::ToggleTheme => {
self.theme_selector(window, cx);
}
Command::ToggleAccount => {
self.account_selector(window, cx);
}
_ => {}
}
}
@@ -450,7 +471,61 @@ impl Workspace {
});
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
fn relay_notification(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const BODY: &str = "Coop cannot found your gossip relay list. \
Maybe you haven't set it yet or relay not responsed";
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return;
};
let entity = nostr.downgrade();
let loading = Rc::new(Cell::new(false));
let note = Notification::new()
.autohide(false)
.icon(IconName::Relay)
.title("Gossip Relays are required")
.content(move |_window, cx| {
v_flex()
.text_sm()
.text_color(cx.theme().text_muted)
.child(SharedString::from(BODY))
.into_any_element()
})
.action(move |_window, _cx| {
let entity = entity.clone();
let public_key = public_key.to_owned();
Button::new("retry")
.label("Retry")
.small()
.primary()
.loading(loading.get())
.disabled(loading.get())
.on_click({
let loading = Rc::clone(&loading);
move |_ev, _window, cx| {
// Set loading state to true
loading.set(true);
// Retry
entity
.update(cx, |this, cx| {
this.ensure_relay_list(&public_key, cx);
})
.ok();
}
})
});
window.push_notification(note, cx);
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
let current_user = signer.public_key();
@@ -529,16 +604,7 @@ impl Workspace {
})
}
fn titlebar_right(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
let chat = ChatRegistry::global(cx);
let Some(pkey) = signer.public_key() else {
return div();
};
fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
.gap_3()
@@ -583,143 +649,6 @@ impl Workspace {
)
}),
)
/*
.child(
h_flex()
.gap_2()
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.map(|this| match inbox_state {
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(
SharedString::from(
"User hasn't configured a messaging relay list",
),
))
}
_ => this,
}),
)
.child(
Button::new("inbox")
.icon(IconName::Inbox)
.tooltip("Inbox")
.small()
.ghost()
.when(inbox_state.subscribing(), |this| this.indicator())
.dropdown_menu(move |this, _window, cx| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&pkey, cx);
let urls: Vec<SharedString> = profile
.messaging_relays()
.iter()
.map(|url| SharedString::from(url.to_string()))
.collect();
// Header
let menu = this.min_w(px(260.)).label("Messaging Relays");
// Content
let menu = urls.into_iter().fold(menu, |this, url| {
this.item(PopupMenuItem::element(move |_window, _cx| {
h_flex()
.px_1()
.w_full()
.gap_2()
.text_sm()
.child(
div().size_1p5().rounded_full().bg(gpui::green()),
)
.child(url.clone())
}))
});
// Footer
menu.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshMessagingRelays),
)
.menu_with_icon(
"Update relays",
IconName::Settings,
Box::new(Command::ShowMessaging),
)
}),
),
)
.child(
h_flex()
.gap_2()
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.map(|this| match nostr.read(cx).relay_list_state {
RelayState::Checking => this
.child(div().child(SharedString::from(
"Fetching user's relay list...",
))),
RelayState::NotConfigured => {
this.child(div().text_color(cx.theme().warning_active).child(
SharedString::from("User hasn't configured a relay list"),
))
}
_ => this,
}),
)
.child(
Button::new("relay-list")
.icon(IconName::Relay)
.tooltip("User's relay list")
.small()
.ghost()
.when(nostr.read(cx).relay_list_state.configured(), |this| {
this.indicator()
})
.dropdown_menu(move |this, _window, cx| {
let nostr = NostrRegistry::global(cx);
let urls: Vec<SharedString> = vec![];
// Header
let menu = this.min_w(px(260.)).label("Relays");
// Content
let menu = urls.into_iter().fold(menu, |this, url| {
this.item(PopupMenuItem::element(move |_window, _cx| {
h_flex()
.px_1()
.w_full()
.gap_2()
.text_sm()
.child(
div().size_1p5().rounded_full().bg(gpui::green()),
)
.child(url.clone())
}))
});
// Footer
menu.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshRelayList),
)
.menu_with_icon(
"Update relay list",
IconName::Settings,
Box::new(Command::ShowRelayList),
)
}),
),
) */
}
}

View File

@@ -1,83 +0,0 @@
use std::collections::{HashMap, HashSet};
use gpui::SharedString;
use nostr_sdk::prelude::*;
/// Gossip
#[derive(Debug, Clone, Default)]
pub struct Gossip {
relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
}
impl Gossip {
pub fn read_only_relays(&self, public_key: &PublicKey) -> Vec<SharedString> {
self.relays
.get(public_key)
.map(|relays| {
relays
.iter()
.map(|(url, _)| url.to_string().into())
.collect()
})
.unwrap_or_default()
}
/// Get read relays for a given public key
pub fn read_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
self.relays
.get(public_key)
.map(|relays| {
relays
.iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
Some(url.to_owned())
} else {
None
}
})
.collect()
})
.unwrap_or_default()
}
/// Get write relays for a given public key
pub fn write_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
self.relays
.get(public_key)
.map(|relays| {
relays
.iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
Some(url.to_owned())
} else {
None
}
})
.collect()
})
.unwrap_or_default()
}
/// Insert gossip relays for a public key
pub fn insert_relays(&mut self, event: &Event) {
self.relays.entry(event.pubkey).or_default().extend(
event
.tags
.iter()
.filter_map(|tag| {
if let Some(TagStandard::RelayMetadata {
relay_url,
metadata,
}) = tag.clone().to_standardized()
{
Some((relay_url, metadata))
} else {
None
}
})
.take(3),
);
}
}

View File

@@ -14,14 +14,12 @@ use nostr_sdk::prelude::*;
mod blossom;
mod constants;
mod device;
mod gossip;
mod nip05;
mod signer;
pub use blossom::*;
pub use constants::*;
pub use device::*;
pub use gossip::*;
pub use nip05::*;
pub use signer::*;
@@ -52,6 +50,8 @@ pub enum StateEvent {
Connected,
/// User has not set up NIP-65 relays
RelayNotConfigured,
/// Connected to NIP-65 relays
RelayConnected,
/// A new signer has been set
SignerSet,
/// An error occurred
@@ -76,7 +76,7 @@ pub struct NostrRegistry {
app_keys: Keys,
/// Tasks for asynchronous operations
tasks: Vec<Task<Result<(), Error>>>,
tasks: Vec<Task<()>>,
}
impl EventEmitter<StateEvent> for NostrRegistry {}
@@ -173,35 +173,42 @@ impl NostrRegistry {
fn connect(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
// Add search relay to the relay pool
for url in SEARCH_RELAYS.into_iter() {
client.add_relay(url).await?;
}
// Add bootstrap relay to the relay pool
for url in BOOTSTRAP_RELAYS.into_iter() {
client.add_relay(url).await?;
}
// Connect to all added relays
client.connect().and_wait(Duration::from_secs(2)).await;
Ok(())
});
// Emit connecting event
cx.emit(StateEvent::Connecting);
self.tasks.push(cx.spawn(async move |this, cx| {
cx.background_executor()
.await_on_background(async move {
// Add search relay to the relay pool
for url in SEARCH_RELAYS.into_iter() {
client.add_relay(url).await.ok();
}
// Add bootstrap relay to the relay pool
for url in BOOTSTRAP_RELAYS.into_iter() {
client.add_relay(url).await.ok();
}
// Connect to all added relays
client.connect().and_wait(Duration::from_secs(2)).await;
})
.await;
// Update the state
this.update(cx, |this, cx| {
cx.emit(StateEvent::Connected);
this.get_npubs(cx);
})?;
Ok(())
}));
self.tasks
.push(cx.spawn(async move |this, cx| match task.await {
Ok(_) => {
this.update(cx, |this, cx| {
cx.emit(StateEvent::Connected);
this.get_npubs(cx);
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
}));
}
/// Get all used npubs
@@ -247,24 +254,26 @@ impl NostrRegistry {
true => {
this.update(cx, |this, cx| {
this.create_identity(cx);
})?;
})
.ok();
}
false => {
// TODO: auto login
npubs.update(cx, |this, cx| {
this.extend(public_keys);
cx.notify();
})?;
npubs
.update(cx, |this, cx| {
this.extend(public_keys);
cx.notify();
})
.ok();
}
},
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})?;
})
.ok();
}
}
Ok(())
}));
}
@@ -337,15 +346,20 @@ impl NostrRegistry {
});
self.tasks.push(cx.spawn(async move |this, cx| {
// Wait for the task to complete
task.await?;
// Set signer
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
})?;
Ok(())
match task.await {
Ok(_) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
};
}));
}
@@ -424,6 +438,7 @@ impl NostrRegistry {
Ok(public_key) => {
// Update states
this.update(cx, |this, cx| {
this.ensure_relay_list(&public_key, cx);
// Add public key to npubs if not already present
this.npubs.update(cx, |this, cx| {
if !this.contains(&public_key) {
@@ -433,16 +448,16 @@ impl NostrRegistry {
});
// Emit signer changed event
cx.emit(StateEvent::SignerSet);
})?;
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})?;
})
.ok();
}
}
Ok(())
};
}));
}
@@ -454,16 +469,15 @@ impl NostrRegistry {
self.tasks.push(cx.spawn(async move |this, cx| {
let key_path = keys_dir.join(format!("{}.npub", npub));
smol::fs::remove_file(key_path).await?;
smol::fs::remove_file(key_path).await.ok();
this.update(cx, |this, cx| {
this.npubs().update(cx, |this, cx| {
this.retain(|k| k != &public_key);
cx.notify();
});
})?;
Ok(())
})
.ok();
}));
}
@@ -481,16 +495,16 @@ impl NostrRegistry {
Ok(_) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
})?;
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})?;
})
.ok();
}
}
Ok(())
};
}));
}
@@ -512,32 +526,88 @@ impl NostrRegistry {
match task.await {
Ok((public_key, uri)) => {
let username = public_key.to_bech32().unwrap();
let write_credential = this.read_with(cx, |_this, cx| {
cx.write_credentials(&username, "nostrconnect", uri.to_string().as_bytes())
})?;
let write_credential = this
.read_with(cx, |_this, cx| {
cx.write_credentials(
&username,
"nostrconnect",
uri.to_string().as_bytes(),
)
})
.unwrap();
match write_credential.await {
Ok(_) => {
this.update(cx, |this, cx| {
this.set_signer(nip46, cx);
})?;
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})?;
})
.ok();
}
}
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})?;
})
.ok();
}
};
}));
}
pub fn ensure_relay_list(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
let task = self.get_event(public_key, Kind::RelayList, cx);
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(_) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::RelayConnected);
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::RelayNotConfigured);
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
};
}));
}
/// Get an event with the given author and kind.
pub fn get_event(
&self,
author: &PublicKey,
kind: Kind,
cx: &App,
) -> Task<Result<Event, Error>> {
let client = self.client();
let public_key = *author;
cx.background_spawn(async move {
let filter = Filter::new().kind(kind).author(public_key).limit(1);
let mut stream = client
.stream_events(filter)
.timeout(Duration::from_millis(800))
.await?;
while let Some((_url, res)) = stream.next().await {
if let Ok(event) = res {
return Ok(event);
}
}
Ok(())
}));
Err(anyhow!("No event found"))
})
}
/// Get the public key of a NIP-05 address