store multi accounts
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m37s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m28s
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-03-01 13:43:18 +07:00
parent 551a0a5093
commit f8e6b3ff7a
8 changed files with 526 additions and 495 deletions

77
Cargo.lock generated
View File

@@ -1198,7 +1198,7 @@ dependencies = [
[[package]]
name = "collections"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"indexmap",
"rustc-hash 2.1.1",
@@ -1644,7 +1644,7 @@ dependencies = [
[[package]]
name = "derive_refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"proc-macro2",
"quote",
@@ -2599,7 +2599,7 @@ dependencies = [
[[package]]
name = "gpui"
version = "0.2.2"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -2678,7 +2678,7 @@ dependencies = [
[[package]]
name = "gpui_linux"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"as-raw-xcb-connection",
@@ -2726,7 +2726,7 @@ dependencies = [
[[package]]
name = "gpui_macos"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"async-task",
@@ -2768,7 +2768,7 @@ dependencies = [
[[package]]
name = "gpui_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -2779,7 +2779,7 @@ dependencies = [
[[package]]
name = "gpui_platform"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"console_error_panic_hook",
"gpui",
@@ -2792,7 +2792,7 @@ dependencies = [
[[package]]
name = "gpui_tokio"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"gpui",
@@ -2803,7 +2803,7 @@ dependencies = [
[[package]]
name = "gpui_util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"log",
@@ -2812,7 +2812,7 @@ dependencies = [
[[package]]
name = "gpui_web"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"console_error_panic_hook",
@@ -2835,7 +2835,7 @@ dependencies = [
[[package]]
name = "gpui_wgpu"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"bytemuck",
@@ -2863,7 +2863,7 @@ dependencies = [
[[package]]
name = "gpui_windows"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"collections",
@@ -3107,7 +3107,7 @@ dependencies = [
[[package]]
name = "http_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"async-compression",
@@ -3132,7 +3132,7 @@ dependencies = [
[[package]]
name = "http_client_tls"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"rustls",
"rustls-platform-verifier",
@@ -3668,12 +3668,13 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
version = "0.1.12"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
dependencies = [
"bitflags 2.11.0",
"libc",
"plain",
"redox_syscall 0.7.3",
]
@@ -3893,7 +3894,7 @@ dependencies = [
[[package]]
name = "media"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"bindgen",
@@ -4120,7 +4121,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "nostr"
version = "0.44.1"
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
dependencies = [
"aes",
"base64",
@@ -4144,7 +4145,7 @@ dependencies = [
[[package]]
name = "nostr-blossom"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
dependencies = [
"base64",
"nostr",
@@ -4155,7 +4156,7 @@ dependencies = [
[[package]]
name = "nostr-connect"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
dependencies = [
"async-utility",
"futures-core",
@@ -4168,7 +4169,7 @@ dependencies = [
[[package]]
name = "nostr-database"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
dependencies = [
"btreecap",
"flatbuffers",
@@ -4178,7 +4179,7 @@ dependencies = [
[[package]]
name = "nostr-gossip"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
dependencies = [
"nostr",
]
@@ -4186,7 +4187,7 @@ dependencies = [
[[package]]
name = "nostr-lmdb"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
dependencies = [
"async-utility",
"flume",
@@ -4200,7 +4201,7 @@ dependencies = [
[[package]]
name = "nostr-sdk"
version = "0.44.1"
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
dependencies = [
"async-utility",
"async-wsocket",
@@ -4637,7 +4638,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "perf"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"collections",
"serde",
@@ -4770,6 +4771,12 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "png"
version = "0.17.16"
@@ -5318,7 +5325,7 @@ dependencies = [
[[package]]
name = "refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"derive_refineable",
]
@@ -5417,7 +5424,7 @@ dependencies = [
[[package]]
name = "reqwest_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"bytes",
@@ -5472,7 +5479,7 @@ dependencies = [
[[package]]
name = "rope"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"arrayvec",
"log",
@@ -5734,7 +5741,7 @@ dependencies = [
[[package]]
name = "scheduler"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"async-task",
"backtrace",
@@ -6328,7 +6335,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "sum_tree"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"arrayvec",
"log",
@@ -7271,7 +7278,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"async-fs",
@@ -7310,7 +7317,7 @@ dependencies = [
[[package]]
name = "util_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"perf",
"quote",
@@ -9113,7 +9120,7 @@ dependencies = [
[[package]]
name = "zlog"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"anyhow",
"chrono",
@@ -9130,7 +9137,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "ztracing"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
dependencies = [
"tracing",
"tracing-subscriber",
@@ -9141,7 +9148,7 @@ dependencies = [
[[package]]
name = "ztracing_macro"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
[[package]]
name = "zune-core"

View File

@@ -262,9 +262,12 @@ impl ChatRegistry {
pub fn get_contact_list(&mut self, cx: &mut Context<Self>) {
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 Some(public_key) = signer.public_key() else {
return;
};
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
@@ -318,9 +321,12 @@ impl ChatRegistry {
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 Some(public_key) = signer.public_key() else {
return Task::ready(Err(anyhow!("User not found")));
};
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
cx.background_spawn(async move {

View File

@@ -331,7 +331,7 @@ impl Room {
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let sender = signer.public_key().unwrap();
let sender = signer.public_key();
// Get room's id
let id = self.id;
@@ -340,7 +340,7 @@ impl Room {
let members: Vec<PublicKey> = self
.members
.iter()
.filter(|public_key| public_key != &&sender)
.filter(|public_key| Some(**public_key) != sender)
.copied()
.collect();

View File

@@ -11,7 +11,7 @@ use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
use crate::panels::{connect, import, messaging_relays, profile, relay_list};
use crate::panels::{messaging_relays, profile, relay_list};
use crate::workspace::Workspace;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
@@ -88,9 +88,6 @@ impl Render for GreeterPanel {
let nostr = NostrRegistry::global(cx);
let nip65 = nostr.read(cx).relay_list_state();
let signer = nostr.read(cx).signer();
let owned = signer.owned();
let required_actions =
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
@@ -191,60 +188,48 @@ impl Render for GreeterPanel {
),
)
})
.when(!owned, |this| {
this.child(
v_flex()
.gap_2()
.w_full()
.child(
h_flex()
.gap_2()
.w_full()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Use your own identity"))
.child(div().flex_1().h_px().bg(cx.theme().border)),
)
.child(
v_flex()
.gap_2()
.w_full()
.child(
Button::new("connect")
.icon(Icon::new(IconName::Door))
.label("Connect account via Nostr Connect")
.ghost()
.small()
.justify_start()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
connect::init(window, cx),
DockPlacement::Center,
window,
cx,
);
}),
)
.child(
Button::new("import")
.icon(Icon::new(IconName::Usb))
.label("Import a secret key or bunker")
.ghost()
.small()
.justify_start()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
import::init(window, cx),
DockPlacement::Center,
window,
cx,
);
}),
),
),
)
})
.child(
v_flex()
.gap_2()
.w_full()
.child(
h_flex()
.gap_2()
.w_full()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Use your own identity"))
.child(div().flex_1().h_px().bg(cx.theme().border)),
)
.child(
v_flex()
.gap_2()
.w_full()
.child(
Button::new("connect")
.icon(Icon::new(IconName::Door))
.label("Connect account via Nostr Connect")
.ghost()
.small()
.justify_start()
.on_click(move |_ev, window, cx| {
//
}),
)
.child(
Button::new("import")
.icon(Icon::new(IconName::Usb))
.label("Import a secret key or bunker")
.ghost()
.small()
.justify_start()
.on_click(move |_ev, window, cx| {
//
}),
),
),
)
.child(
v_flex()
.gap_2()

View File

@@ -1,8 +1,6 @@
pub mod backup;
pub mod connect;
pub mod contact_list;
pub mod greeter;
pub mod import;
pub mod messaging_relays;
pub mod profile;
pub mod relay_list;

View File

@@ -204,9 +204,11 @@ impl DeviceRegistry {
fn subscribe_to_giftwrap_events(&mut self, cx: &mut Context<Self>) -> Task<Result<(), 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 Some(public_key) = signer.public_key() else {
return Task::ready(Err(anyhow!("User not found")));
};
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
@@ -237,9 +239,11 @@ impl DeviceRegistry {
pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
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 Some(public_key) = signer.public_key() else {
return;
};
// Reset state before fetching announcement
self.reset(cx);
@@ -303,10 +307,11 @@ impl DeviceRegistry {
pub fn create_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get current user
let signer = nostr.read(cx).signer();
let public_key = signer.public_key().unwrap();
let Some(public_key) = signer.public_key() else {
return Task::ready(Err(anyhow!("User not found")));
};
// Get user's write relays
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
@@ -398,9 +403,11 @@ impl DeviceRegistry {
pub fn listen_request(&mut self, cx: &mut Context<Self>) {
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 Some(public_key) = signer.public_key() else {
return;
};
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
@@ -430,9 +437,11 @@ impl DeviceRegistry {
fn listen_approval(&mut self, cx: &mut Context<Self>) {
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 Some(public_key) = signer.public_key() else {
return;
};
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
@@ -460,9 +469,11 @@ impl DeviceRegistry {
fn request(&mut self, cx: &mut Context<Self>) {
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 Some(public_key) = signer.public_key() else {
return;
};
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
@@ -573,10 +584,11 @@ impl DeviceRegistry {
fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get current user
let signer = nostr.read(cx).signer();
let public_key = signer.public_key().unwrap();
let Some(public_key) = signer.public_key() else {
return;
};
// Get user's write relays
let write_relays = nostr.read(cx).write_relays(&public_key, cx);

View File

@@ -52,7 +52,7 @@ pub struct NostrRegistry {
signer: Arc<CoopSigner>,
/// Local public keys
npubs: Entity<HashSet<PublicKey>>,
npubs: Entity<Vec<PublicKey>>,
/// App keys
///
@@ -93,7 +93,7 @@ impl NostrRegistry {
let signer = Arc::new(CoopSigner::new(app_keys.clone()));
// Construct the nostr npubs entity
let npubs = cx.new(|_| HashSet::new());
let npubs = cx.new(|_| vec![]);
// Construct the gossip entity
let gossip = cx.new(|_| Gossip::default());
@@ -121,7 +121,6 @@ impl NostrRegistry {
cx.defer_in(window, |this, _window, cx| {
this.connect(cx);
this.handle_notifications(cx);
this.get_npubs(cx);
});
Self {
@@ -137,43 +136,48 @@ impl NostrRegistry {
}
}
/// Get all used npubs
fn get_npubs(&mut self, cx: &mut Context<Self>) {
self.tasks.push(cx.spawn(async move |this, cx| {
let task: Result<HashSet<PublicKey>, Error> = cx
.background_executor()
.await_on_background(async move {
let dir = config_dir().join("keys");
// Ensure keys directory exists
smol::fs::create_dir_all(&dir).await?;
let mut files = smol::fs::read_dir(&dir).await?;
let mut npubs: HashSet<PublicKey> = HashSet::new();
while let Some(Ok(entry)) = files.next().await {
let name = entry.file_name().into_string().unwrap();
let public_key = PublicKey::parse(&name)?;
npubs.insert(public_key);
}
Ok(npubs)
})
.await;
if let Ok(npubs) = task {
this.update(cx, |this, cx| {
this.npubs.update(cx, |this, cx| {
this.extend(npubs);
cx.notify();
});
})?;
}
Ok(())
}));
/// Get the nostr client
pub fn client(&self) -> Client {
self.client.clone()
}
/// Get the nostr signer
pub fn signer(&self) -> Arc<CoopSigner> {
self.signer.clone()
}
/// Get the app keys
pub fn app_keys(&self) -> &Keys {
&self.app_keys
}
/// Get the connected status of the client
pub fn connected(&self) -> bool {
self.connected
}
/// Get the creating status
pub fn creating(&self) -> bool {
self.creating
}
/// Get the relay list state
pub fn relay_list_state(&self) -> RelayState {
self.relay_list_state.clone()
}
/// Get all relays for a given public key without ensuring connections
pub fn read_only_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<SharedString> {
self.gossip.read(cx).read_only_relays(public_key)
}
/// Set the connected status of the client
fn set_connected(&mut self, cx: &mut Context<Self>) {
self.connected = true;
cx.notify();
}
/// Connect to the bootstrapping relays
fn connect(&mut self, cx: &mut Context<Self>) {
let client = self.client();
@@ -198,7 +202,7 @@ impl NostrRegistry {
// Update the state
this.update(cx, |this, cx| {
this.set_connected(cx);
this.get_signer(cx);
this.get_npubs(cx);
})?;
Ok(())
@@ -259,39 +263,370 @@ impl NostrRegistry {
}));
}
/// Get the nostr client
pub fn client(&self) -> Client {
self.client.clone()
/// Get all used npubs
fn get_npubs(&mut self, cx: &mut Context<Self>) {
let npubs = self.npubs.downgrade();
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
let dir = config_dir().join("keys");
// Ensure keys directory exists
smol::fs::create_dir_all(&dir).await?;
let mut files = smol::fs::read_dir(&dir).await?;
let mut entries = Vec::new();
while let Some(Ok(entry)) = files.next().await {
let metadata = entry.metadata().await?;
let modified_time = metadata.modified()?;
let name = entry
.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));
let mut npubs = Vec::new();
for (_, name) in entries {
let public_key = PublicKey::parse(&name)?;
npubs.push(public_key);
}
Ok(npubs)
});
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(public_keys) => match public_keys.is_empty() {
true => {
this.update(cx, |this, cx| {
this.create_new_signer(cx);
})?;
}
false => {
npubs.update(cx, |this, cx| {
this.extend(public_keys);
cx.notify();
})?;
}
},
Err(e) => {
log::error!("Failed to get npubs: {e}");
this.update(cx, |this, cx| {
this.create_new_signer(cx);
})?;
}
}
Ok(())
}));
}
/// Get the nostr signer
pub fn signer(&self) -> Arc<CoopSigner> {
self.signer.clone()
/// Set whether Coop is creating a new signer
fn set_creating(&mut self, creating: bool, cx: &mut Context<Self>) {
self.creating = creating;
cx.notify();
}
/// Get the app keys
pub fn app_keys(&self) -> &Keys {
&self.app_keys
/// Create a new identity
pub fn create_new_signer(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let keys = Keys::generate();
let async_keys = keys.clone();
let username = keys.public_key().to_bech32().unwrap();
let secret = keys.secret_key().to_secret_bytes();
// Create a write credential task
let write_credential = cx.write_credentials(&username, &username, &secret);
// Set the creating signer status
self.set_creating(true, cx);
// Run async tasks in background
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = async_keys.into_nostr_signer();
// Get default relay list
let relay_list = default_relay_list();
// Publish relay list event
let event = EventBuilder::relay_list(relay_list).sign(&signer).await?;
client
.send_event(&event)
.ok_timeout(Duration::from_secs(TIMEOUT))
.await?;
// Construct the default metadata
let name = petname::petname(2, "-").unwrap_or("Cooper".to_string());
let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap();
let metadata = Metadata::new().display_name(&name).picture(avatar);
// Publish metadata event
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
client
.send_event(&event)
.ack_policy(AckPolicy::none())
.await?;
// Construct the default contact list
let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())];
// Publish contact list event
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
client
.send_event(&event)
.ack_policy(AckPolicy::none())
.await?;
// Construct the default messaging relay list
let relays = default_messaging_relays();
// Publish messaging relay list event
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
client
.send_event(&event)
.ack_policy(AckPolicy::none())
.await?;
// Write user's credentials to the system keyring
write_credential.await?;
Ok(())
});
self.tasks.push(cx.spawn(async move |this, cx| {
// Wait for the task to complete
task.await?;
this.update(cx, |this, cx| {
this.set_creating(false, cx);
this.set_signer(keys, cx);
})?;
Ok(())
}));
}
/// Get the connected status of the client
pub fn connected(&self) -> bool {
self.connected
// Get the signer in keyring by username
pub fn get_signer(
&mut self,
username: &str,
cx: &mut Context<Self>,
) -> Task<Result<Arc<dyn NostrSigner>, Error>> {
let app_keys = self.app_keys().clone();
let read_credential = cx.read_credentials(username);
cx.spawn(async move |_this, _cx| {
let (_, secret) = read_credential
.await
.map_err(|_| anyhow!("Failed to get signer"))?
.ok_or_else(|| anyhow!("Failed to get signer"))?;
// 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(120);
let nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
Ok(nip46.into_nostr_signer())
})
}
/// Get the creating status
pub fn creating(&self) -> bool {
self.creating
/// Set the signer for the nostr client and verify the public key
pub fn set_signer<T>(&mut self, new: T, cx: &mut Context<Self>)
where
T: NostrSigner + 'static,
{
let client = self.client();
let signer = self.signer();
// Create a task to update the signer and verify the public key
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
// Update signer and unsubscribe
signer.switch(new).await;
client.unsubscribe_all().await?;
// Verify and save public key
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let npub = public_key.to_bech32().unwrap();
let keys_dir = config_dir().join("keys");
// Ensure keys directory exists
smol::fs::create_dir_all(&keys_dir).await?;
let key_path = keys_dir.join(format!("{}.npub", npub));
smol::fs::write(key_path, "").await?;
log::info!("Signer's public key: {}", public_key);
Ok(public_key)
});
self.tasks.push(cx.spawn(async move |this, cx| {
// set signer
let public_key = task.await?;
// Update states
this.update(cx, |this, cx| {
this.npubs.update(cx, |this, cx| {
if !this.contains(&public_key) {
this.push(public_key);
cx.notify();
}
});
this.ensure_relay_list(cx);
})?;
Ok(())
}));
}
/// Get the relay list state
pub fn relay_list_state(&self) -> RelayState {
self.relay_list_state.clone()
/// Add a key signer to keyring
pub fn add_key_signer(
&mut self,
keys: &Keys,
cx: &mut Context<Self>,
) -> Task<Result<(), Error>> {
let keys = keys.clone();
let username = keys.public_key().to_bech32().unwrap();
let secret = keys.secret_key().to_secret_bytes();
// Write the credential to the keyring
let write_credential = cx.write_credentials(&username, &username, &secret);
cx.spawn(async move |this, cx| {
match write_credential.await {
Ok(_) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
})?;
}
Err(e) => return Err(anyhow!("Failed to write credential: {e}")),
}
Ok(())
})
}
/// Get all relays for a given public key without ensuring connections
pub fn read_only_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<SharedString> {
self.gossip.read(cx).read_only_relays(public_key)
/// Add a nostr connect signer to keyring
pub fn add_nip46_signer(
&mut self,
nip46: &NostrConnect,
cx: &mut Context<Self>,
) -> Task<Result<(), Error>> {
let nip46 = nip46.clone();
let async_nip46 = nip46.clone();
// Connect and verify the remote signer
let task: Task<Result<(PublicKey, NostrConnectUri), Error>> =
cx.background_spawn(async move {
let public_key = async_nip46.get_public_key().await?;
let uri = async_nip46.bunker_uri().await?;
Ok((public_key, uri))
});
cx.spawn(async move |this, cx| {
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, &username, uri.to_string().as_bytes())
})?;
match write_credential.await {
Ok(_) => {
this.update(cx, |this, cx| {
this.set_signer(nip46, cx);
})?;
}
Err(e) => return Err(anyhow!("Failed to write credential: {e}")),
}
}
Err(e) => return Err(anyhow!("Failed to connect to the remote signer: {e}")),
}
Ok(())
})
}
/// Set the state of the relay list
fn set_relay_state(&mut self, state: RelayState, cx: &mut Context<Self>) {
self.relay_list_state = state;
cx.notify();
}
pub fn ensure_relay_list(&mut self, cx: &mut Context<Self>) {
let task = self.verify_relay_list(cx);
// Set the state to idle before starting the task
self.set_relay_state(RelayState::default(), cx);
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await?;
// Update state
this.update(cx, |this, cx| {
this.relay_list_state = result;
cx.notify();
})?;
Ok(())
}));
}
// Verify relay list for current user
fn verify_relay_list(&mut self, cx: &mut Context<Self>) -> Task<Result<RelayState, Error>> {
let client = self.client();
cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
.limit(1);
// Construct target for subscription
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
.into_iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect();
// Stream events from the bootstrap relays
let mut stream = client
.stream_events(target)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
log::info!("Received relay list event: {event:?}");
return Ok(RelayState::Configured);
}
Err(e) => {
log::error!("Failed to receive relay list event: {e}");
}
}
}
Ok(RelayState::NotConfigured)
})
}
/// Ensure write relays for a given public key
@@ -381,289 +716,8 @@ impl NostrRegistry {
})
}
/// Set the connected status of the client
fn set_connected(&mut self, cx: &mut Context<Self>) {
self.connected = true;
cx.notify();
}
/// Get local stored signer
fn get_signer(&mut self, cx: &mut Context<Self>) {
let read_credential = cx.read_credentials(KEYRING);
self.tasks.push(cx.spawn(async move |this, cx| {
match read_credential.await {
Ok(Some((_user, secret))) => {
let secret = SecretKey::from_slice(&secret)?;
let keys = Keys::new(secret);
this.update(cx, |this, cx| {
this.set_signer(keys, false, cx);
})?;
}
_ => {
this.update(cx, |this, cx| {
this.get_bunker(cx);
})?;
}
}
Ok(())
}));
}
/// Get local stored bunker connection
fn get_bunker(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let app_keys = self.app_keys().clone();
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
let task: Task<Result<NostrConnect, Error>> = cx.background_spawn(async move {
log::info!("Getting bunker connection");
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.identifier("coop:account")
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
let uri = NostrConnectUri::parse(event.content)?;
let signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None)?;
Ok(signer)
} else {
Err(anyhow!("No account found"))
}
});
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(signer) => {
this.update(cx, |this, cx| {
this.set_signer(signer, true, cx);
})
.ok();
}
Err(e) => {
log::warn!("Failed to get bunker: {e}");
// Create a new identity if no stored bunker exists
this.update(cx, |this, cx| {
this.set_default_signer(cx);
})
.ok();
}
}
Ok(())
}));
}
/// Set the signer for the nostr client and verify the public key
pub fn set_signer<T>(&mut self, new: T, owned: bool, cx: &mut Context<Self>)
where
T: NostrSigner + 'static,
{
let client = self.client();
let signer = self.signer();
// Create a task to update the signer and verify the public key
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
// Update signer and unsubscribe
signer.switch(new, owned).await;
client.unsubscribe_all().await?;
// Verify and save public key
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let npub = public_key.to_bech32().unwrap();
let keys_dir = config_dir().join("keys");
// Ensure keys directory exists
smol::fs::create_dir_all(&keys_dir).await?;
let key_path = keys_dir.join(format!("{}.npub", npub));
smol::fs::write(key_path, "").await?;
log::info!("Signer's public key: {}", public_key);
Ok(public_key)
});
self.tasks.push(cx.spawn(async move |this, cx| {
// set signer
let public_key = task.await?;
// Update states
this.update(cx, |this, cx| {
this.npubs.update(cx, |this, cx| {
this.insert(public_key);
cx.notify();
});
this.ensure_relay_list(cx);
})?;
Ok(())
}));
}
/// Create a new identity
fn set_default_signer(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let keys = Keys::generate();
let async_keys = keys.clone();
// Create a write credential task
let write_credential = cx.write_credentials(
KEYRING,
&keys.public_key().to_hex(),
&keys.secret_key().to_secret_bytes(),
);
// Set the creating signer status
self.set_creating_signer(true, cx);
// Run async tasks in background
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = async_keys.into_nostr_signer();
// Get default relay list
let relay_list = default_relay_list();
// Publish relay list event
let event = EventBuilder::relay_list(relay_list).sign(&signer).await?;
client
.send_event(&event)
.ok_timeout(Duration::from_secs(TIMEOUT))
.await?;
// Construct the default metadata
let name = petname::petname(2, "-").unwrap_or("Cooper".to_string());
let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap();
let metadata = Metadata::new().display_name(&name).picture(avatar);
// Publish metadata event
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
client
.send_event(&event)
.ok_timeout(Duration::from_secs(TIMEOUT))
.ack_policy(AckPolicy::none())
.await?;
// Construct the default contact list
let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())];
// Publish contact list event
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
client
.send_event(&event)
.ok_timeout(Duration::from_secs(TIMEOUT))
.ack_policy(AckPolicy::none())
.await?;
// Construct the default messaging relay list
let relays = default_messaging_relays();
// Publish messaging relay list event
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
client
.send_event(&event)
.ok_timeout(Duration::from_secs(TIMEOUT))
.ack_policy(AckPolicy::none())
.await?;
// Write user's credentials to the system keyring
write_credential.await?;
Ok(())
});
self.tasks.push(cx.spawn(async move |this, cx| {
// Wait for the task to complete
task.await?;
this.update(cx, |this, cx| {
this.set_creating_signer(false, cx);
this.set_signer(keys, false, cx);
})?;
Ok(())
}));
}
/// Set whether Coop is creating a new signer
fn set_creating_signer(&mut self, creating: bool, cx: &mut Context<Self>) {
self.creating = creating;
cx.notify();
}
/// Set the state of the relay list
fn set_relay_state(&mut self, state: RelayState, cx: &mut Context<Self>) {
self.relay_list_state = state;
cx.notify();
}
pub fn ensure_relay_list(&mut self, cx: &mut Context<Self>) {
let task = self.verify_relay_list(cx);
// Set the state to idle before starting the task
self.set_relay_state(RelayState::default(), cx);
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await?;
// Update state
this.update(cx, |this, cx| {
this.relay_list_state = result;
cx.notify();
})?;
Ok(())
}));
}
// Verify relay list for current user
fn verify_relay_list(&mut self, cx: &mut Context<Self>) -> Task<Result<RelayState, Error>> {
let client = self.client();
cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
.limit(1);
// Construct target for subscription
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
.into_iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect();
// Stream events from the bootstrap relays
let mut stream = client
.stream_events(target)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
log::info!("Received relay list event: {event:?}");
return Ok(RelayState::Configured);
}
Err(e) => {
log::error!("Failed to receive relay list event: {e}");
}
}
}
Ok(RelayState::NotConfigured)
})
}
/// Generate a direct nostr connection initiated by the client
pub fn client_connect(&self, relay: Option<RelayUrl>) -> (NostrConnect, NostrConnectUri) {
pub fn nostr_connect(&self, relay: Option<RelayUrl>) -> (NostrConnect, NostrConnectUri) {
let app_keys = self.app_keys();
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
@@ -685,25 +739,6 @@ impl NostrRegistry {
(signer, uri)
}
/// Store the bunker connection for the next login
pub fn persist_bunker(&mut self, uri: NostrConnectUri, cx: &mut App) {
let client = self.client();
let rng_keys = Keys::generate();
self.tasks.push(cx.background_spawn(async move {
// Construct the event for application-specific data
let event = EventBuilder::new(Kind::ApplicationSpecificData, uri.to_string())
.tag(Tag::identifier("coop:account"))
.sign(&rng_keys)
.await?;
// Store the event in the database
client.database().save_event(&event).await?;
Ok(())
}));
}
/// Get the public key of a NIP-05 address
pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
let client = self.client();

View File

@@ -1,6 +1,5 @@
use std::borrow::Cow;
use std::result::Result;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use nostr_sdk::prelude::*;
@@ -16,11 +15,6 @@ pub struct CoopSigner {
/// Specific signer for encryption purposes
encryption_signer: RwLock<Option<Arc<dyn NostrSigner>>>,
/// By default, Coop generates a new signer for new users.
///
/// This flag indicates whether the signer is user-owned or Coop-generated.
owned: AtomicBool,
}
impl CoopSigner {
@@ -32,7 +26,6 @@ impl CoopSigner {
signer: RwLock::new(signer.into_nostr_signer()),
signer_pkey: RwLock::new(None),
encryption_signer: RwLock::new(None),
owned: AtomicBool::new(false),
}
}
@@ -47,17 +40,15 @@ impl CoopSigner {
}
/// Get public key
///
/// Ensure to call this method after the signer has been initialized.
/// Otherwise, this method will panic.
pub fn public_key(&self) -> Option<PublicKey> {
self.signer_pkey.read_blocking().to_owned()
}
/// Get the flag indicating whether the signer is user-owned.
pub fn owned(&self) -> bool {
self.owned.load(Ordering::SeqCst)
*self.signer_pkey.read_blocking()
}
/// Switch the current signer to a new signer.
pub async fn switch<T>(&self, new: T, owned: bool)
pub async fn switch<T>(&self, new: T)
where
T: IntoNostrSigner,
{
@@ -75,9 +66,6 @@ impl CoopSigner {
// Reset the encryption signer
*encryption_signer = None;
// Update the owned flag
self.owned.store(owned, Ordering::SeqCst);
}
/// Set the encryption signer.