chore: simplify codebase and prepare for multi-platforms #28
@@ -128,22 +128,17 @@ impl ChatRegistry {
|
|||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe to the signer event
|
// Subscribe to the signer event
|
||||||
cx.subscribe(&nostr, |this, _state, event, cx| {
|
cx.subscribe(&nostr, |this, _state, event, cx| {
|
||||||
match event {
|
if event == &StateEvent::SignerSet {
|
||||||
StateEvent::SignerSet => {
|
|
||||||
this.reset(cx);
|
this.reset(cx);
|
||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
}
|
|
||||||
StateEvent::RelayConnected => {
|
|
||||||
this.get_contact_list(cx);
|
|
||||||
this.get_messages(cx)
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Run at the end of the current cycle
|
// Run at the end of the current cycle
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
|
this.get_contact_list(cx);
|
||||||
|
this.get_messages(cx);
|
||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
this.handle_notifications(cx);
|
this.handle_notifications(cx);
|
||||||
this.tracking(cx);
|
this.tracking(cx);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::cell::Cell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ::settings::AppSettings;
|
use ::settings::AppSettings;
|
||||||
@@ -24,7 +22,7 @@ use ui::button::{Button, ButtonVariants};
|
|||||||
use ui::dock::{ClosePanel, DockArea, DockItem, DockPlacement, PanelView};
|
use ui::dock::{ClosePanel, DockArea, DockItem, DockPlacement, PanelView};
|
||||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||||
use ui::notification::{Notification, NotificationKind};
|
use ui::notification::{Notification, NotificationKind};
|
||||||
use ui::{Disableable, Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
use crate::dialogs::restore::RestoreEncryption;
|
use crate::dialogs::restore::RestoreEncryption;
|
||||||
use crate::dialogs::{accounts, settings};
|
use crate::dialogs::{accounts, settings};
|
||||||
@@ -51,7 +49,6 @@ enum Command {
|
|||||||
ToggleTheme,
|
ToggleTheme,
|
||||||
ToggleAccount,
|
ToggleAccount,
|
||||||
|
|
||||||
RefreshRelayList,
|
|
||||||
RefreshMessagingRelays,
|
RefreshMessagingRelays,
|
||||||
BackupEncryption,
|
BackupEncryption,
|
||||||
ImportEncryption,
|
ImportEncryption,
|
||||||
@@ -73,14 +70,8 @@ pub struct Workspace {
|
|||||||
/// App's Dock Area
|
/// App's Dock Area
|
||||||
dock: Entity<DockArea>,
|
dock: Entity<DockArea>,
|
||||||
|
|
||||||
/// Whether a user's relay list is connected
|
|
||||||
relay_connected: bool,
|
|
||||||
|
|
||||||
/// Whether the inbox is connected
|
|
||||||
inbox_connected: bool,
|
|
||||||
|
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 6]>,
|
_subscriptions: SmallVec<[Subscription; 5]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
@@ -88,7 +79,6 @@ impl Workspace {
|
|||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let device = DeviceRegistry::global(cx);
|
let device = DeviceRegistry::global(cx);
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let npubs = nostr.read(cx).npubs();
|
|
||||||
|
|
||||||
let titlebar = cx.new(|_| TitleBar::new());
|
let titlebar = cx.new(|_| TitleBar::new());
|
||||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||||
@@ -102,15 +92,6 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Observe the npubs entity
|
|
||||||
cx.observe_in(&npubs, window, move |this, npubs, window, cx| {
|
|
||||||
if !npubs.read(cx).is_empty() {
|
|
||||||
this.account_selector(window, cx);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
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| {
|
||||||
@@ -141,25 +122,8 @@ impl Workspace {
|
|||||||
|
|
||||||
window.push_notification(note, cx);
|
window.push_notification(note, cx);
|
||||||
}
|
}
|
||||||
StateEvent::FetchingRelayList => {
|
|
||||||
let note = Notification::new()
|
|
||||||
.id::<RelayNotifcation>()
|
|
||||||
.message("Getting relay list...")
|
|
||||||
.with_kind(NotificationKind::Info);
|
|
||||||
|
|
||||||
window.push_notification(note, cx);
|
|
||||||
}
|
|
||||||
StateEvent::RelayNotConfigured => {
|
|
||||||
this.relay_warning(window, cx);
|
|
||||||
}
|
|
||||||
StateEvent::RelayConnected => {
|
|
||||||
window.clear_notification::<RelayNotifcation>(cx);
|
|
||||||
this.set_relay_connected(true, cx);
|
|
||||||
}
|
|
||||||
StateEvent::SignerSet => {
|
StateEvent::SignerSet => {
|
||||||
this.set_center_layout(window, cx);
|
this.set_center_layout(window, cx);
|
||||||
this.set_relay_connected(false, cx);
|
|
||||||
this.set_inbox_connected(false, cx);
|
|
||||||
// Clear the signer notification
|
// Clear the signer notification
|
||||||
window.clear_notification::<SignerNotifcation>(cx);
|
window.clear_notification::<SignerNotifcation>(cx);
|
||||||
}
|
}
|
||||||
@@ -255,9 +219,6 @@ impl Workspace {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ChatEvent::Subscribed => {
|
|
||||||
this.set_inbox_connected(true, cx);
|
|
||||||
}
|
|
||||||
ChatEvent::Error(error) => {
|
ChatEvent::Error(error) => {
|
||||||
window.push_notification(Notification::error(error).autohide(false), cx);
|
window.push_notification(Notification::error(error).autohide(false), cx);
|
||||||
}
|
}
|
||||||
@@ -285,8 +246,6 @@ impl Workspace {
|
|||||||
Self {
|
Self {
|
||||||
titlebar,
|
titlebar,
|
||||||
dock,
|
dock,
|
||||||
relay_connected: false,
|
|
||||||
inbox_connected: false,
|
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,18 +277,6 @@ impl Workspace {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set whether the relay list is connected
|
|
||||||
fn set_relay_connected(&mut self, connected: bool, cx: &mut Context<Self>) {
|
|
||||||
self.relay_connected = connected;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set whether the inbox is connected
|
|
||||||
fn set_inbox_connected(&mut self, connected: bool, cx: &mut Context<Self>) {
|
|
||||||
self.inbox_connected = connected;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the dock layout
|
/// Set the dock layout
|
||||||
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
||||||
@@ -428,16 +375,6 @@ impl Workspace {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Command::RefreshRelayList => {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let signer = nostr.read(cx).signer();
|
|
||||||
|
|
||||||
if let Some(public_key) = signer.public_key() {
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.ensure_relay_list(&public_key, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::RefreshEncryption => {
|
Command::RefreshEncryption => {
|
||||||
let device = DeviceRegistry::global(cx);
|
let device = DeviceRegistry::global(cx);
|
||||||
device.update(cx, |this, cx| {
|
device.update(cx, |this, cx| {
|
||||||
@@ -630,55 +567,6 @@ impl Workspace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn relay_warning(&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)
|
|
||||||
.id::<RelayNotifcation>()
|
|
||||||
.icon(IconName::Relay)
|
|
||||||
.title("Gossip Relays are required")
|
|
||||||
.message(BODY)
|
|
||||||
.action(move |_this, _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, cx: &mut Context<Self>) -> impl IntoElement {
|
fn titlebar_left(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
@@ -759,9 +647,6 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let relay_connected = self.relay_connected;
|
|
||||||
let inbox_connected = self.inbox_connected;
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
@@ -889,12 +774,6 @@ impl Workspace {
|
|||||||
.icon(IconName::Inbox)
|
.icon(IconName::Inbox)
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.loading(!inbox_connected)
|
|
||||||
.disabled(!inbox_connected)
|
|
||||||
.when(!inbox_connected, |this| {
|
|
||||||
this.tooltip("Connecting to the user's messaging relays...")
|
|
||||||
})
|
|
||||||
.when(inbox_connected, |this| this.indicator())
|
|
||||||
.dropdown_menu(move |this, _window, cx| {
|
.dropdown_menu(move |this, _window, cx| {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
@@ -950,38 +829,17 @@ impl Workspace {
|
|||||||
Box::new(Command::RefreshMessagingRelays),
|
Box::new(Command::RefreshMessagingRelays),
|
||||||
)
|
)
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
"Update relays",
|
"Update gossip relays",
|
||||||
|
IconName::Relay,
|
||||||
|
Box::new(Command::ShowRelayList),
|
||||||
|
)
|
||||||
|
.menu_with_icon(
|
||||||
|
"Update messaging relays",
|
||||||
IconName::Settings,
|
IconName::Settings,
|
||||||
Box::new(Command::ShowMessaging),
|
Box::new(Command::ShowMessaging),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
|
||||||
Button::new("relay-list")
|
|
||||||
.icon(IconName::Relay)
|
|
||||||
.small()
|
|
||||||
.ghost()
|
|
||||||
.loading(!relay_connected)
|
|
||||||
.disabled(!relay_connected)
|
|
||||||
.when(!relay_connected, |this| {
|
|
||||||
this.tooltip("Connecting to the user's relay list...")
|
|
||||||
})
|
|
||||||
.when(relay_connected, |this| this.indicator())
|
|
||||||
.dropdown_menu(move |this, _window, _cx| {
|
|
||||||
this.label("User's Relay List")
|
|
||||||
.separator()
|
|
||||||
.menu_with_icon(
|
|
||||||
"Reload",
|
|
||||||
IconName::Refresh,
|
|
||||||
Box::new(Command::RefreshRelayList),
|
|
||||||
)
|
|
||||||
.menu_with_icon(
|
|
||||||
"Update",
|
|
||||||
IconName::Settings,
|
|
||||||
Box::new(Command::ShowRelayList),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,18 +111,10 @@ 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 nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
// Get announcement when signer is set
|
|
||||||
let subscription = cx.subscribe_in(&nostr, window, |this, _e, event, _window, cx| {
|
let subscription = cx.subscribe_in(&nostr, window, |this, _e, event, _window, cx| {
|
||||||
match event {
|
if event == &StateEvent::SignerSet {
|
||||||
StateEvent::SignerSet => {
|
|
||||||
this.set_subscribing(false, cx);
|
this.set_subscribing(false, cx);
|
||||||
this.set_requesting(false, cx);
|
this.set_requesting(false, cx);
|
||||||
}
|
|
||||||
StateEvent::RelayConnected => {
|
|
||||||
this.get_announcement(cx);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,6 +139,7 @@ impl DeviceRegistry {
|
|||||||
self.tasks.push(cx.background_spawn(async move {
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
let mut notifications = client.notifications();
|
let mut notifications = client.notifications();
|
||||||
let mut processed_events = HashSet::new();
|
let mut processed_events = HashSet::new();
|
||||||
|
let mut found_relay_list = false;
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
while let Some(notification) = notifications.next().await {
|
||||||
if let ClientNotification::Message { message, .. } = notification
|
if let ClientNotification::Message { message, .. } = notification
|
||||||
@@ -158,6 +151,17 @@ impl DeviceRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match event.kind {
|
match event.kind {
|
||||||
|
Kind::RelayList => {
|
||||||
|
// Skip if the relay list has already been found
|
||||||
|
if found_relay_list {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Verify the relay list event is signed by the user's signer
|
||||||
|
if verify_author(&client, event.as_ref()).await {
|
||||||
|
tx.send_async(event.into_owned()).await?;
|
||||||
|
found_relay_list = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Kind::Custom(4454) => {
|
Kind::Custom(4454) => {
|
||||||
if verify_author(&client, event.as_ref()).await {
|
if verify_author(&client, event.as_ref()).await {
|
||||||
tx.send_async(event.into_owned()).await?;
|
tx.send_async(event.into_owned()).await?;
|
||||||
|
|||||||
@@ -44,18 +44,12 @@ impl Global for GlobalNostrRegistry {}
|
|||||||
/// Signer event.
|
/// Signer event.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum StateEvent {
|
pub enum StateEvent {
|
||||||
/// Creating the signer
|
|
||||||
Creating,
|
|
||||||
/// Connecting to the bootstrapping relay
|
/// Connecting to the bootstrapping relay
|
||||||
Connecting,
|
Connecting,
|
||||||
/// Connected to the bootstrapping relay
|
/// Connected to the bootstrapping relay
|
||||||
Connected,
|
Connected,
|
||||||
/// Fetching the relay list
|
/// Creating the signer
|
||||||
FetchingRelayList,
|
Creating,
|
||||||
/// User has not set up NIP-65 relays
|
|
||||||
RelayNotConfigured,
|
|
||||||
/// Connected to NIP-65 relays
|
|
||||||
RelayConnected,
|
|
||||||
/// A new signer has been set
|
/// A new signer has been set
|
||||||
SignerSet,
|
SignerSet,
|
||||||
/// An error occurred
|
/// An error occurred
|
||||||
@@ -154,6 +148,10 @@ impl NostrRegistry {
|
|||||||
// 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| {
|
||||||
this.connect(cx);
|
this.connect(cx);
|
||||||
|
// Create an identity if none exists
|
||||||
|
if this.npubs.read(cx).is_empty() {
|
||||||
|
this.create_identity(cx);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -465,7 +463,6 @@ impl NostrRegistry {
|
|||||||
{
|
{
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
let signer = self.signer();
|
let signer = self.signer();
|
||||||
let key_dir = self.key_dir.clone();
|
|
||||||
|
|
||||||
// Create a task to update the signer and verify the public key
|
// Create a task to update the signer and verify the public key
|
||||||
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||||
@@ -578,118 +575,6 @@ impl NostrRegistry {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure the relay list is fetched for the given public key
|
|
||||||
pub fn ensure_relay_list(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
|
||||||
let task = self.get_event(public_key, Kind::RelayList, cx);
|
|
||||||
|
|
||||||
// Emit a fetching event before starting the task
|
|
||||||
cx.emit(StateEvent::FetchingRelayList);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
match task.await {
|
|
||||||
Ok(event) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.ensure_connection(&event, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.update(cx, |_this, cx| {
|
|
||||||
cx.emit(StateEvent::RelayNotConfigured);
|
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure that the user is connected to the relay specified in the NIP-65 event.
|
|
||||||
pub fn ensure_connection(&mut self, event: &Event, cx: &mut Context<Self>) {
|
|
||||||
let client = self.client();
|
|
||||||
// Extract the relay list from the event
|
|
||||||
let relays: Vec<(RelayUrl, Option<RelayMetadata>)> = nip65::extract_relay_list(event)
|
|
||||||
.map(|(url, metadata)| (url.to_owned(), metadata.to_owned()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
|
||||||
for (url, metadata) in relays.into_iter() {
|
|
||||||
match metadata {
|
|
||||||
Some(RelayMetadata::Read) => {
|
|
||||||
client
|
|
||||||
.add_relay(url)
|
|
||||||
.capabilities(RelayCapabilities::READ)
|
|
||||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.and_connect()
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Some(RelayMetadata::Write) => {
|
|
||||||
client
|
|
||||||
.add_relay(url)
|
|
||||||
.capabilities(RelayCapabilities::WRITE)
|
|
||||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.and_connect()
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
client
|
|
||||||
.add_relay(url)
|
|
||||||
.capabilities(RelayCapabilities::NONE)
|
|
||||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.and_connect()
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
match task.await {
|
|
||||||
Ok(_) => {
|
|
||||||
this.update(cx, |_this, cx| {
|
|
||||||
cx.emit(StateEvent::RelayConnected);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.update(cx, |_this, cx| {
|
|
||||||
cx.emit(StateEvent::RelayNotConfigured);
|
|
||||||
cx.emit(StateEvent::error(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow!("No event found"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the public key of a NIP-05 address
|
/// Get the public key of a NIP-05 address
|
||||||
pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
|
pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
|
||||||
use common::config_dir;
|
|
||||||
use gpui::{App, Context};
|
|
||||||
use nostr_connect::prelude::*;
|
|
||||||
|
|
||||||
use crate::{CLIENT_NAME, NOSTR_CONNECT_TIMEOUT};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct NostrRing {
|
|
||||||
/// Keys directory
|
|
||||||
dir: PathBuf,
|
|
||||||
|
|
||||||
/// Master app keys used for various operations.
|
|
||||||
///
|
|
||||||
/// Example: Nostr Connect and NIP-4e operations
|
|
||||||
app_keys: Keys,
|
|
||||||
|
|
||||||
/// All local stored identities
|
|
||||||
npubs: Vec<PublicKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NostrRing {
|
|
||||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
|
||||||
let dir = config_dir().join("keys");
|
|
||||||
let app_keys = get_or_init_app_keys(cx).unwrap_or(Keys::generate());
|
|
||||||
|
|
||||||
// Get all local stored npubs
|
|
||||||
let npubs = match Self::discover(&dir) {
|
|
||||||
Ok(npubs) => npubs,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to discover npubs: {e}");
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
dir,
|
|
||||||
app_keys,
|
|
||||||
npubs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the secret for a given npub, if it exists
|
|
||||||
fn get_secret(&self, public_key: &PublicKey, cx: &App) -> Result<Arc<dyn NostrSigner>, Error> {
|
|
||||||
let npub = public_key.to_bech32()?;
|
|
||||||
let key_path = self.dir.join(format!("{}.npub", npub));
|
|
||||||
|
|
||||||
if let Ok(secret) = std::fs::read_to_string(key_path) {
|
|
||||||
let secret = SecretKey::parse(&secret)?;
|
|
||||||
let keys = Keys::new(secret);
|
|
||||||
|
|
||||||
Ok(keys.into_nostr_signer())
|
|
||||||
} else {
|
|
||||||
self.get_secret_keyring(&npub, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the secret for a given npub in the os credentials store
|
|
||||||
#[deprecated = "Use get_secret instead"]
|
|
||||||
fn get_secret_keyring(&self, user: &str, cx: &App) -> Result<Arc<dyn NostrSigner>, Error> {
|
|
||||||
let read = cx.read_credentials(user);
|
|
||||||
let app_keys = self.app_keys.clone();
|
|
||||||
|
|
||||||
cx.foreground_executor().block_on(async move {
|
|
||||||
let (_, secret) = read
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))?
|
|
||||||
.ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?;
|
|
||||||
|
|
||||||
// Try to parse as a direct secret key first
|
|
||||||
if let Ok(secret_key) = SecretKey::from_slice(&secret) {
|
|
||||||
return Ok(Keys::new(secret_key).into_nostr_signer());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the secret into string
|
|
||||||
let sec = String::from_utf8(secret)
|
|
||||||
.map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?;
|
|
||||||
|
|
||||||
// Try to parse as a NIP-46 URI
|
|
||||||
let uri =
|
|
||||||
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
|
|
||||||
|
|
||||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
|
||||||
let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
|
|
||||||
|
|
||||||
// Set the auth URL handler
|
|
||||||
nip46.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
Ok(nip46.into_nostr_signer())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new npub to the keys directory
|
|
||||||
fn add(&mut self, public_key: PublicKey, secret: &str) -> Result<(), Error> {
|
|
||||||
let npub = public_key.to_bech32()?;
|
|
||||||
let key_path = self.dir.join(format!("{}.npub", npub));
|
|
||||||
std::fs::write(key_path, secret)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a npub from the keys directory
|
|
||||||
fn remove(&self, public_key: &PublicKey) -> Result<(), Error> {
|
|
||||||
let npub = public_key.to_bech32()?;
|
|
||||||
let key_path = self.dir.join(format!("{}.npub", npub));
|
|
||||||
std::fs::remove_file(key_path)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Discover all npubs in the keys directory
|
|
||||||
fn discover(dir: &PathBuf) -> Result<Vec<PublicKey>, Error> {
|
|
||||||
// Ensure keys directory exists
|
|
||||||
std::fs::create_dir_all(dir)?;
|
|
||||||
|
|
||||||
let files = std::fs::read_dir(dir)?;
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
let mut npubs: Vec<PublicKey> = Vec::new();
|
|
||||||
|
|
||||||
for file in files.flatten() {
|
|
||||||
let metadata = file.metadata()?;
|
|
||||||
let modified_time = metadata.modified()?;
|
|
||||||
let name = file.file_name().into_string().unwrap().replace(".npub", "");
|
|
||||||
entries.push((modified_time, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by modification time (most recent first)
|
|
||||||
entries.sort_by(|a, b| b.0.cmp(&a.0));
|
|
||||||
|
|
||||||
for (_, name) in entries {
|
|
||||||
let public_key = PublicKey::parse(&name)?;
|
|
||||||
npubs.push(public_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(npubs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user