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

This commit is contained in:
2026-02-06 13:25:34 +07:00
parent fce4c1bbcd
commit 253d04f988
17 changed files with 1081 additions and 872 deletions

View File

@@ -169,7 +169,6 @@ impl CommandBar {
fn search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
let query = self.find_input.read(cx).value();
// Return if the query is empty
@@ -191,7 +190,7 @@ impl CommandBar {
// Block the input until the search completes
self.set_finding(true, window, cx);
let find_users = if identity.read(cx).owned {
let find_users = if nostr.read(cx).owned_signer() {
nostr.read(cx).wot_search(&query, cx)
} else {
nostr.read(cx).search(&query, cx)
@@ -245,17 +244,28 @@ impl CommandBar {
fn create(&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 async_chat = chat.downgrade();
let nostr = NostrRegistry::global(cx);
let signer_pkey = nostr.read(cx).signer_pkey(cx);
// Get all selected public keys
let receivers = self.selected(cx);
chat.update(cx, |this, cx| {
let room = cx.new(|_| Room::new(public_key, receivers));
this.emit_room(room.downgrade(), cx);
let task: Task<Result<(), Error>> = cx.spawn_in(window, async move |_this, cx| {
let public_key = signer_pkey.await?;
async_chat.update_in(cx, |this, window, cx| {
let room = cx.new(|_| Room::new(public_key, receivers));
this.emit_room(room.downgrade(), cx);
window.close_modal(cx);
})?;
Ok(())
});
window.close_modal(cx);
task.detach();
}
fn select(&mut self, pkey: PublicKey, cx: &mut Context<Self>) {

View File

@@ -29,6 +29,26 @@ impl GreeterPanel {
focus_handle: cx.focus_handle(),
}
}
fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let signer_pkey = nostr.read(cx).signer_pkey(cx);
cx.spawn_in(window, async move |_this, cx| {
if let Ok(public_key) = signer_pkey.await {
cx.update(|window, cx| {
Workspace::add_panel(
profile::init(public_key, window, cx),
DockPlacement::Center,
window,
cx,
);
})
.ok();
}
})
.detach();
}
}
impl Panel for GreeterPanel {
@@ -62,12 +82,11 @@ impl Render for GreeterPanel {
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
let nip65_state = nostr.read(cx).nip65_state();
let nip17_state = nostr.read(cx).nip17_state();
let relay_list_state = identity.read(cx).relay_list_state();
let messaging_relay_state = identity.read(cx).messaging_relays_state();
let required_actions =
relay_list_state == RelayState::NotSet || messaging_relay_state == RelayState::NotSet;
let required_actions = nip65_state.read(cx) == &RelayState::NotConfigured
|| nip17_state.read(cx) == &RelayState::NotConfigured;
h_flex()
.size_full()
@@ -128,7 +147,7 @@ impl Render for GreeterPanel {
v_flex()
.gap_2()
.w_full()
.when(relay_list_state == RelayState::NotSet, |this| {
.when(nip65_state.read(cx).not_configured(), |this| {
this.child(
Button::new("relaylist")
.icon(Icon::new(IconName::Relay))
@@ -146,31 +165,28 @@ impl Render for GreeterPanel {
}),
)
})
.when(
messaging_relay_state == RelayState::NotSet,
|this| {
this.child(
Button::new("import")
.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),
DockPlacement::Center,
window,
cx,
);
}),
)
},
),
.when(nip17_state.read(cx).not_configured(), |this| {
this.child(
Button::new("import")
.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),
DockPlacement::Center,
window,
cx,
);
}),
)
}),
),
)
})
.when(!identity.read(cx).owned, |this| {
.when(!nostr.read(cx).owned_signer(), |this| {
this.child(
v_flex()
.gap_2()
@@ -257,14 +273,9 @@ impl Render for GreeterPanel {
.ghost()
.small()
.no_center()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
profile::init(window, cx),
DockPlacement::Center,
window,
cx,
);
}),
.on_click(cx.listener(move |this, _ev, window, cx| {
this.add_profile_panel(window, cx)
})),
)
.child(
Button::new("invite")

View File

@@ -156,29 +156,20 @@ impl MessagingRelayPanel {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let relays = self.relays.clone();
let tags: Vec<Tag> = self
.relays
.iter()
.map(|relay| Tag::relay(relay.clone()))
.collect();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
let tags: Vec<Tag> = relays
.iter()
.map(|relay| Tag::relay(relay.clone()))
.collect();
// Construct nip17 event builder
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
let event = client.sign_event_builder(builder).await?;
// Set messaging relays
client.send_event(&event).to(urls).await?;
// Connect to messaging relays
for relay in relays.iter() {
client.add_relay(relay).await.ok();
client.connect_relay(relay).await.ok();
}
client.send_event(&event).to_nip65().await?;
Ok(())
});

View File

@@ -22,8 +22,8 @@ use ui::input::{InputState, TextInput};
use ui::notification::Notification;
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
cx.new(|cx| ProfilePanel::new(window, cx))
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
cx.new(|cx| ProfilePanel::new(public_key, window, cx))
}
#[derive(Debug)]
@@ -31,6 +31,9 @@ pub struct ProfilePanel {
name: SharedString,
focus_handle: FocusHandle,
/// User's public key
public_key: PublicKey,
/// User's name text input
name_input: Entity<InputState>,
@@ -51,13 +54,10 @@ pub struct ProfilePanel {
}
impl ProfilePanel {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me"));
// Hidden input for avatar url
let avatar_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/a.jpg"));
// Use multi-line input for bio
let bio_input = cx.new(|cx| {
InputState::new(window, cx)
@@ -66,13 +66,10 @@ impl ProfilePanel {
.placeholder("A short introduce about you.")
});
// Get user's profile and update inputs
cx.defer_in(window, move |this, window, cx| {
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
// Set all input's values with current profile
this.set_profile(profile, window, cx);
});
@@ -80,6 +77,7 @@ impl ProfilePanel {
Self {
name: "Update Profile".into(),
focus_handle: cx.focus_handle(),
public_key,
name_input,
avatar_input,
bio_input,
@@ -209,7 +207,7 @@ impl ProfilePanel {
fn set_metadata(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let public_key = self.public_key;
// Get the old metadata
let persons = PersonRegistry::global(cx);
@@ -289,9 +287,7 @@ impl Focusable for ProfilePanel {
impl Render for ProfilePanel {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let shorten_pkey = SharedString::from(shorten_pubkey(public_key, 8));
let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8));
// Get the avatar
let avatar_input = self.avatar_input.read(cx).value();
@@ -390,7 +386,7 @@ impl Render for ProfilePanel {
.ghost()
.on_click(cx.listener(move |this, _ev, window, cx| {
this.copy(
public_key.to_bech32().unwrap(),
this.public_key.to_bech32().unwrap(),
window,
cx,
);

View File

@@ -9,10 +9,11 @@ use gpui::{
div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, Styled, Subscription, Window,
};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
use titlebar::TitleBar;
use ui::avatar::Avatar;
use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension};
@@ -35,6 +36,9 @@ pub struct Workspace {
/// App's Command Bar
command_bar: Entity<CommandBar>,
/// Current User
current_user: Entity<Option<PublicKey>>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 3]>,
}
@@ -42,20 +46,23 @@ pub struct Workspace {
impl Workspace {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
let current_user = cx.new(|_| None);
let nostr = NostrRegistry::global(cx);
let nip65_state = nostr.read(cx).nip65_state();
// Titlebar
let titlebar = cx.new(|_| TitleBar::new());
// Command bar
let command_bar = cx.new(|cx| CommandBar::new(window, cx));
// Dock
let dock =
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
let mut subscriptions = smallvec![];
subscriptions.push(
// Automatically sync theme with system appearance
window.observe_window_appearance(|window, cx| {
Theme::sync_system_appearance(Some(window), cx);
}),
);
subscriptions.push(
// Observe all events emitted by the chat registry
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
@@ -100,6 +107,15 @@ impl Workspace {
}),
);
subscriptions.push(
// Observe the NIP-65 state
cx.observe(&nip65_state, move |this, state, cx| {
if state.read(cx).idle() {
this.get_current_user(cx);
}
}),
);
// Set the default layout for app's dock
cx.defer_in(window, |this, window, cx| {
this.set_layout(window, cx);
@@ -109,6 +125,7 @@ impl Workspace {
titlebar,
dock,
command_bar,
current_user,
_subscriptions: subscriptions,
}
}
@@ -173,18 +190,35 @@ impl Workspace {
});
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
fn get_current_user(&self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
let client = nostr.read(cx).client();
let current_user = self.current_user.downgrade();
cx.spawn(async move |_this, cx| {
if let Some(signer) = client.signer() {
if let Ok(public_key) = signer.get_public_key().await {
current_user
.update(cx, |this, cx| {
*this = Some(public_key);
cx.notify();
})
.ok();
}
}
})
.detach();
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
h_flex()
.h(TITLEBAR_HEIGHT)
.flex_1()
.justify_between()
.gap_2()
.when_some(identity.read(cx).public_key, |this, public_key| {
.when_some(self.current_user.read(cx).as_ref(), |this, public_key| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
let profile = persons.read(cx).get(public_key, cx);
this.child(
h_flex()