.
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m25s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m46s

This commit is contained in:
2026-02-04 08:42:03 +07:00
parent cc7efd2864
commit 07c5d58f8e
9 changed files with 199 additions and 563 deletions

135
Cargo.lock generated
View File

@@ -94,6 +94,56 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.100"
@@ -1097,6 +1147,46 @@ dependencies = [
"libloading",
]
[[package]]
name = "clap"
version = "4.5.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "clap_lex"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "cmake"
version = "0.1.57"
@@ -1192,6 +1282,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "combine"
version = "4.6.7"
@@ -3274,6 +3370,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.13.0"
@@ -4246,6 +4348,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oneshot"
version = "0.1.13"
@@ -4508,6 +4616,20 @@ dependencies = [
"state",
]
[[package]]
name = "petname"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068"
dependencies = [
"anyhow",
"clap",
"itertools 0.14.0",
"proc-macro2",
"quote",
"rand 0.8.5",
]
[[package]]
name = "phf"
version = "0.11.3"
@@ -6074,6 +6196,7 @@ dependencies = [
"nostr-connect",
"nostr-lmdb",
"nostr-sdk",
"petname",
"reqwest",
"rustls",
"serde",
@@ -6097,6 +6220,12 @@ dependencies = [
"float-cmp",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
@@ -7102,6 +7231,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "util"
version = "0.1.0"

3
assets/icons/shield.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M20.25 6.94155C20.25 6.08069 19.6991 5.31641 18.8825 5.04418L12.6325 2.96085C12.2219 2.824 11.7781 2.824 11.3675 2.96085L5.11754 5.04418C4.30086 5.31641 3.75 6.08069 3.75 6.94155V11.9124C3.75 16.8848 8 19.25 12 21.4079C16 19.25 20.25 16.8848 20.25 11.9124V6.94155Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 446 B

View File

@@ -104,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);
@@ -166,7 +166,7 @@ impl ChatRegistry {
Self {
rooms: vec![],
loading: true,
loading: false,
sender: tx.clone(),
tracking_flag,
tracking: None,
@@ -253,37 +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.tracking = Some(cx.background_spawn(async move {
let loop_duration = Duration::from_secs(12);
let mut total_loops = 0;
loop {
if client.has_signer().await {
total_loops += 1;
if status.load(Ordering::Acquire) {
// Reset gift wrap processing flag
_ = status.compare_exchange(
true,
false,
Ordering::Release,
Ordering::Relaxed,
);
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
tx.send_async(NostrEvent::Unwrapping(true)).await.ok();
} else {
// Wait at least 2 loops to prevent exiting early while events are still being processed
if total_loops >= 2 {
tx.send_async(NostrEvent::Unwrapping(false)).await.ok();
// Reset the counter
total_loops = 0;
}
}
}
smol::Timer::after(loop_duration).await;
}

View File

@@ -12,6 +12,7 @@ const SECONDS_IN_MINUTE: i64 = 60;
const MINUTES_IN_HOUR: i64 = 60;
const HOURS_IN_DAY: i64 = 24;
const DAYS_IN_MONTH: i64 = 30;
const IMAGE_RESIZER: &str = "https://wsrv.nl";
pub trait RenderedProfile {
fn avatar(&self) -> SharedString;
@@ -24,7 +25,12 @@ impl RenderedProfile for Profile {
.picture
.as_ref()
.filter(|picture| !picture.is_empty())
.map(|picture| picture.into())
.map(|picture| {
let url = format!(
"{IMAGE_RESIZER}/?url={picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
);
url.into()
})
.unwrap_or_else(|| "brand/avatar.png".into())
}

View File

@@ -1,510 +0,0 @@
use std::ops::Range;
use std::time::Duration;
use anyhow::{anyhow, Error};
use chat::{ChatRegistry, Room};
use common::{TextUtils, BOOTSTRAP_RELAYS};
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
IntoElement, ParentElement, Render, RetainAllImageCache, SharedString,
StatefulInteractiveElement, Styled, Subscription, Task, Window,
};
use gpui_tokio::Tokio;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use state::{NostrAddress, NostrRegistry};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::input::{InputEvent, InputState, TextInput};
use ui::modal::ModalButtonProps;
use ui::notification::Notification;
use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension};
pub fn compose_button() -> impl IntoElement {
div().child(
Button::new("compose")
.icon(IconName::Plus)
.ghost_alt()
.cta()
.small()
.rounded()
.on_click(move |_, window, cx| {
let compose = cx.new(|cx| Compose::new(window, cx));
let weak_view = compose.downgrade();
window.open_modal(cx, move |modal, _window, cx| {
let weak_view = weak_view.clone();
let label = if compose.read(cx).selected(cx).len() > 1 {
SharedString::from("Create Group DM")
} else {
SharedString::from("Create DM")
};
modal
.alert()
.overlay_closable(true)
.keyboard(true)
.show_close(true)
.button_props(ModalButtonProps::default().ok_text(label))
.title(SharedString::from("Direct Messages"))
.child(compose.clone())
.on_ok(move |_, window, cx| {
weak_view
.update(cx, |this, cx| {
this.submit(window, cx);
})
.ok();
// false to prevent the modal from closing
false
})
})
}),
)
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Contact {
public_key: PublicKey,
selected: bool,
}
impl AsRef<PublicKey> for Contact {
fn as_ref(&self) -> &PublicKey {
&self.public_key
}
}
impl Contact {
pub fn new(public_key: PublicKey) -> Self {
Self {
public_key,
selected: false,
}
}
pub fn selected(mut self) -> Self {
self.selected = true;
self
}
}
pub struct Compose {
/// Input for the room's subject
title_input: Entity<InputState>,
/// Input for the room's members
user_input: Entity<InputState>,
/// User's contacts
contacts: Entity<Vec<Contact>>,
/// Error message
error_message: Entity<Option<SharedString>>,
image_cache: Entity<RetainAllImageCache>,
_subscriptions: SmallVec<[Subscription; 2]>,
_tasks: SmallVec<[Task<()>; 1]>,
}
impl Compose {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let contacts = cx.new(|_| vec![]);
let error_message = cx.new(|_| None);
let user_input =
cx.new(|cx| InputState::new(window, cx).placeholder("npub or nprofile..."));
let title_input =
cx.new(|cx| InputState::new(window, cx).placeholder("Family...(Optional)"));
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
let get_contacts: Task<Result<Vec<Contact>, Error>> = cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let profiles = client.database().contacts(public_key).await?;
let contacts: Vec<Contact> = profiles
.into_iter()
.map(|profile| Contact::new(profile.public_key()))
.collect();
Ok(contacts)
});
tasks.push(
// Load all contacts
cx.spawn_in(window, async move |this, cx| {
match get_contacts.await {
Ok(contacts) => {
this.update(cx, |this, cx| {
this.extend_contacts(contacts, cx);
})
.ok();
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx);
})
.ok();
}
};
}),
);
subscriptions.push(
// Clear the image cache when sidebar is closed
cx.on_release_in(window, move |this, window, cx| {
this.image_cache.update(cx, |this, cx| {
this.clear(window, cx);
})
}),
);
subscriptions.push(
// Handle Enter event for user input
cx.subscribe_in(
&user_input,
window,
move |this, _input, event, window, cx| {
if let InputEvent::PressEnter { .. } = event {
this.add_and_select_contact(window, cx)
};
},
),
);
Self {
title_input,
user_input,
error_message,
contacts,
image_cache: RetainAllImageCache::new(cx),
_subscriptions: subscriptions,
_tasks: tasks,
}
}
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList];
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?;
Ok(())
}
fn extend_contacts<I>(&mut self, contacts: I, cx: &mut Context<Self>)
where
I: IntoIterator<Item = Contact>,
{
self.contacts.update(cx, |this, cx| {
this.extend(contacts);
cx.notify();
});
}
fn push_contact(&mut self, contact: Contact, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let pk = contact.public_key;
if !self.contacts.read(cx).iter().any(|c| c.public_key == pk) {
self._tasks.push(cx.background_spawn(async move {
Self::request_metadata(&client, pk).await.ok();
}));
cx.defer_in(window, |this, window, cx| {
this.contacts.update(cx, |this, cx| {
this.insert(0, contact);
cx.notify();
});
this.user_input.update(cx, |this, cx| {
this.set_value("", window, cx);
this.set_loading(false, cx);
});
});
} else {
self.set_error("Contact already added", cx);
}
}
fn select_contact(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
self.contacts.update(cx, |this, cx| {
if let Some(contact) = this.iter_mut().find(|c| c.public_key == public_key) {
contact.selected = true;
}
cx.notify();
});
}
fn add_and_select_contact(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let content = self.user_input.read(cx).value().to_string();
let http_client = cx.http_client();
// Show loading indicator in the input
self.user_input.update(cx, |this, cx| {
this.set_loading(true, cx);
});
if let Ok(public_key) = content.to_public_key() {
let contact = Contact::new(public_key).selected();
self.push_contact(contact, window, cx);
} else if let Ok(addr) = Nip05Address::parse(&content) {
let task = Tokio::spawn(cx, async move {
if let Ok(profile) = addr.profile(&http_client).await {
let public_key = profile.public_key;
let contact = Contact::new(public_key).selected();
Ok(contact)
} else {
Err(anyhow!("Not found"))
}
});
cx.spawn_in(window, async move |this, cx| {
match task.await {
Ok(Ok(contact)) => {
this.update_in(cx, |this, window, cx| {
this.push_contact(contact, window, cx);
})
.ok();
}
Ok(Err(e)) => {
this.update(cx, |this, cx| {
this.set_error(e.to_string(), cx);
})
.ok();
}
Err(e) => {
log::error!("Tokio error: {e}");
}
};
})
.detach();
}
}
fn selected(&self, cx: &App) -> Vec<PublicKey> {
self.contacts
.read(cx)
.iter()
.filter_map(|contact| {
if contact.selected {
Some(contact.public_key)
} else {
None
}
})
.collect()
}
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let chat = ChatRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let receivers: Vec<PublicKey> = self.selected(cx);
let subject_input = self.title_input.read(cx).value();
let subject = (!subject_input.is_empty()).then(|| subject_input.to_string());
if !self.user_input.read(cx).value().is_empty() {
self.add_and_select_contact(window, cx);
return;
};
chat.update(cx, |this, cx| {
let room = cx.new(|_| Room::new(subject, public_key, receivers));
this.emit_room(room.downgrade(), cx);
});
window.close_modal(cx);
}
fn set_error(&mut self, error: impl Into<SharedString>, cx: &mut Context<Self>) {
// Unlock the user input
self.user_input.update(cx, |this, cx| {
this.set_loading(false, cx);
});
// Update error message
self.error_message.update(cx, |this, cx| {
*this = Some(error.into());
cx.notify();
});
// Dismiss error after 2 seconds
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
this.update(cx, |this, cx| {
this.error_message.update(cx, |this, cx| {
*this = None;
cx.notify();
});
})
.ok();
})
.detach();
}
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
let persons = PersonRegistry::global(cx);
let mut items = Vec::with_capacity(self.contacts.read(cx).len());
for ix in range {
let Some(contact) = self.contacts.read(cx).get(ix) else {
continue;
};
let public_key = contact.public_key;
let profile = persons.read(cx).get(&public_key, cx);
items.push(
h_flex()
.id(ix)
.px_2()
.h_11()
.w_full()
.justify_between()
.rounded(cx.theme().radius)
.child(
h_flex()
.gap_1p5()
.text_sm()
.child(Avatar::new(profile.avatar()).size(rems(1.75)))
.child(profile.name()),
)
.when(contact.selected, |this| {
this.child(
Icon::new(IconName::CheckCircle)
.small()
.text_color(cx.theme().text_accent),
)
})
.hover(|this| this.bg(cx.theme().elevated_surface_background))
.on_click(cx.listener(move |this, _, _window, cx| {
this.select_contact(public_key, cx);
})),
);
}
items
}
}
impl Render for Compose {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let error = self.error_message.read(cx).as_ref();
let loading = self.user_input.read(cx).loading;
let contacts = self.contacts.read(cx);
v_flex()
.image_cache(self.image_cache.clone())
.gap_2()
.child(
div()
.text_sm()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).")),
)
.when_some(error, |this, msg| {
this.child(
div()
.italic()
.text_sm()
.text_color(cx.theme().danger_foreground)
.child(msg.clone()),
)
})
.child(
h_flex()
.gap_1()
.h_10()
.border_b_1()
.border_color(cx.theme().border)
.child(
div()
.text_sm()
.font_semibold()
.child(SharedString::from("Subject:")),
)
.child(TextInput::new(&self.title_input).small().appearance(false)),
)
.child(
v_flex()
.pt_1()
.gap_2()
.child(
v_flex()
.gap_2()
.child(
div()
.text_sm()
.font_semibold()
.child(SharedString::from("To:")),
)
.child(
TextInput::new(&self.user_input)
.small()
.disabled(loading)
.suffix(
Button::new("add")
.icon(IconName::PlusCircle)
.transparent()
.small()
.disabled(loading)
.on_click(cx.listener(move |this, _, window, cx| {
this.add_and_select_contact(window, cx);
})),
),
),
)
.map(|this| {
if contacts.is_empty() {
this.child(
v_flex()
.h_24()
.w_full()
.items_center()
.justify_center()
.text_center()
.text_xs()
.child(
div()
.font_semibold()
.line_height(relative(1.2))
.child(SharedString::from("No contacts")),
)
.child(
div()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Your recently contacts will appear here.")),
),
)
} else {
this.child(
uniform_list(
"contacts",
contacts.len(),
cx.processor(move |this, range, _window, cx| {
this.list_items(range, cx)
}),
)
.h(px(300.)),
)
}
}),
)
}
}

View File

@@ -114,7 +114,6 @@ impl Render for GreeterPanel {
v_flex()
.gap_2()
.w_full()
.items_start()
.child(
h_flex()
.gap_1()
@@ -127,17 +126,16 @@ impl Render for GreeterPanel {
)
.child(
v_flex()
.w_full()
.items_start()
.justify_start()
.gap_2()
.w_full()
.when(relay_list_state == RelayState::NotSet, |this| {
this.child(
Button::new("relaylist")
.icon(Icon::new(IconName::Door))
.icon(Icon::new(IconName::Relay))
.label("Set up relay list")
.ghost()
.small()
.no_center()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
relay_list::init(window, cx),
@@ -153,10 +151,11 @@ impl Render for GreeterPanel {
|this| {
this.child(
Button::new("import")
.icon(Icon::new(IconName::Usb))
.icon(Icon::new(IconName::Relay))
.label("Set up messaging relays")
.ghost()
.small()
.no_center()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
messaging_relays::init(window, cx),
@@ -176,7 +175,6 @@ impl Render for GreeterPanel {
v_flex()
.gap_2()
.w_full()
.items_start()
.child(
h_flex()
.gap_1()
@@ -189,16 +187,15 @@ impl Render for GreeterPanel {
)
.child(
v_flex()
.w_full()
.items_start()
.justify_start()
.gap_2()
.w_full()
.child(
Button::new("connect")
.icon(Icon::new(IconName::Door))
.label("Connect account via Nostr Connect")
.ghost()
.small()
.no_center()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
connect::init(window, cx),
@@ -214,6 +211,7 @@ impl Render for GreeterPanel {
.label("Import a secret key or bunker")
.ghost()
.small()
.no_center()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
import::init(window, cx),
@@ -230,7 +228,6 @@ impl Render for GreeterPanel {
v_flex()
.gap_2()
.w_full()
.items_start()
.child(
h_flex()
.gap_1()
@@ -243,16 +240,23 @@ impl Render for GreeterPanel {
)
.child(
v_flex()
.w_full()
.items_start()
.justify_start()
.gap_2()
.w_full()
.child(
Button::new("backup")
.icon(Icon::new(IconName::Shield))
.label("Backup account")
.ghost()
.small()
.no_center(),
)
.child(
Button::new("profile")
.icon(Icon::new(IconName::Profile))
.label("Update profile")
.ghost()
.small()
.no_center()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
profile::init(window, cx),
@@ -262,19 +266,13 @@ impl Render for GreeterPanel {
);
}),
)
.child(
Button::new("changelog")
.icon(Icon::new(IconName::Ship))
.label("Keep up to date")
.ghost()
.small(),
)
.child(
Button::new("invite")
.icon(Icon::new(IconName::Invite))
.label("Invite people")
.label("Invite friends")
.ghost()
.small(),
.small()
.no_center(),
),
),
),

View File

@@ -23,3 +23,4 @@ serde.workspace = true
serde_json.workspace = true
rustls = "0.23"
petname = "2.0.2"

View File

@@ -37,6 +37,8 @@ pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps";
/// Default subscription id for user gift wrap events
pub const USER_GIFTWRAP: &str = "user-gift-wraps";
/// Default avatar for new users
pub const DEFAULT_AVATAR: &str = "https://image.nostr.build/93bb6084457a42620849b6827f3f34f111ae5a4ac728638a989d4ed4b4bb3ac8.png";
/// Default vertex relays
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
/// Default search relays
@@ -726,7 +728,12 @@ impl NostrRegistry {
/// Create a new identity
fn create_identity(&mut self, cx: &mut Context<Self>) {
let client = self.client();
// Generate new keys
let keys = Keys::generate();
// Get write credential task
let write_credential = cx.write_credentials(
CLIENT_NAME,
&keys.public_key().to_hex(),
@@ -736,10 +743,23 @@ impl NostrRegistry {
// Update the signer
self.set_signer(keys, false, cx);
// TODO: set metadata
// Spawn a task to write the credentials
// Spawn a task to set metadata and write the credentials
cx.background_spawn(async move {
let name = petname::petname(2, "-").unwrap_or("Cooper".to_string());
let avatar = Url::parse(DEFAULT_AVATAR).unwrap();
// Construct metadata for the identity
let metadata = Metadata::new()
.display_name(&name)
.name(&name)
.picture(avatar);
// Set metadata for the identity
if let Err(e) = client.set_metadata(&metadata).await {
log::error!("Failed to set metadata: {}", e);
}
// Write the credentials
if let Err(e) = write_credential.await {
log::error!("Failed to write credentials: {}", e);
}

View File

@@ -42,6 +42,7 @@ pub enum IconName {
Settings,
Sun,
Ship,
Shield,
Upload,
Usb,
PanelLeft,
@@ -96,6 +97,7 @@ impl IconName {
Self::Settings => "icons/settings.svg",
Self::Sun => "icons/sun.svg",
Self::Ship => "icons/ship.svg",
Self::Shield => "icons/shield.svg",
Self::Upload => "icons/upload.svg",
Self::Usb => "icons/usb.svg",
Self::PanelLeft => "icons/panel-left.svg",