update messaging relays panel
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m41s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m46s
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-02-23 19:23:56 +07:00
parent 2ec98e14d0
commit ebf0e86828
2 changed files with 246 additions and 217 deletions

View File

@@ -4,9 +4,9 @@ use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use gpui::prelude::FluentBuilder;
use gpui::{
div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter,
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
Styled, Subscription, Task, TextAlign, UniformList, Window,
div, rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window,
};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
@@ -15,7 +15,10 @@ use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput};
use ui::{divider, h_flex, v_flex, IconName, Sizable, StyledExt};
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
const MSG: &str = "Messaging Relays are relays that hosted all your messages. \
Other users will find your relays and send messages to it.";
pub fn init(window: &mut Window, cx: &mut App) -> Entity<MessagingRelayPanel> {
cx.new(|cx| MessagingRelayPanel::new(window, cx))
@@ -29,44 +32,26 @@ pub struct MessagingRelayPanel {
/// Relay URL input
input: Entity<InputState>,
/// Whether the panel is updating
updating: bool,
/// Error message
error: Option<SharedString>,
// All relays
/// All relays
relays: HashSet<RelayUrl>,
// Event subscriptions
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
// Background tasks
_tasks: SmallVec<[Task<()>; 1]>,
/// Background tasks
tasks: Vec<Task<Result<(), Error>>>,
}
impl MessagingRelayPanel {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
tasks.push(
// Load user's relays in the local database
cx.spawn_in(window, async move |this, cx| {
let result = cx
.background_spawn(async move { Self::load(&client).await })
.await;
if let Ok(relays) = result {
this.update(cx, |this, cx| {
this.relays.extend(relays);
cx.notify();
})
.ok();
}
}),
);
subscriptions.push(
// Subscribe to user's input events
@@ -77,31 +62,54 @@ impl MessagingRelayPanel {
}),
);
// Run at the end of current cycle
cx.defer_in(window, |this, window, cx| {
this.load(window, cx);
});
Self {
name: "Update Messaging Relays".into(),
focus_handle: cx.focus_handle(),
input,
updating: false,
relays: HashSet::new(),
error: None,
_subscriptions: subscriptions,
_tasks: tasks,
tasks: vec![],
}
}
async fn load(client: &Client) -> Result<Vec<RelayUrl>, Error> {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
if let Some(event) = client.database().query(filter).await?.first_owned() {
Ok(nip17::extract_owned_relay_list(event).collect())
} else {
Err(anyhow!("Not found."))
}
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
Ok(nip17::extract_owned_relay_list(event).collect())
} else {
Err(anyhow!("Not found."))
}
});
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let relays = task.await?;
// Update state
this.update(cx, |this, cx| {
this.relays.extend(relays);
cx.notify();
})?;
Ok(())
}));
}
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -113,7 +121,7 @@ impl MessagingRelayPanel {
}
if let Ok(url) = RelayUrl::parse(&value) {
if !self.relays.insert(url) {
if self.relays.insert(url) {
self.input.update(cx, |this, cx| {
this.set_value("", window, cx);
});
@@ -148,6 +156,11 @@ impl MessagingRelayPanel {
.detach();
}
fn set_updating(&mut self, updating: bool, cx: &mut Context<Self>) {
self.updating = updating;
cx.notify();
}
pub fn set_relays(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.relays.is_empty() {
self.set_error("You need to add at least 1 relay", window, cx);
@@ -156,99 +169,99 @@ impl MessagingRelayPanel {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
window.push_notification("Public Key not found", cx);
return;
};
// Get user's write relays
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
// Construct event tags
let tags: Vec<Tag> = self
.relays
.iter()
.map(|relay| Tag::relay(relay.clone()))
.collect();
// Set updating state
self.set_updating(true, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// 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_nip65().await?;
client.send_event(&event).to(urls).await?;
Ok(())
});
cx.spawn_in(window, async move |this, cx| {
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
match task.await {
Ok(_) => {
// TODO
this.update_in(cx, |this, window, cx| {
this.set_updating(false, cx);
this.load(window, cx);
window.push_notification("Update successful", cx);
})?;
}
Err(e) => {
this.update_in(cx, |this, window, cx| {
this.set_error(e.to_string(), window, cx);
})
.ok();
})?;
}
};
})
.detach();
Ok(())
}));
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> UniformList {
let relays = self.relays.clone();
let total = relays.len();
fn render_list_items(&mut self, cx: &mut Context<Self>) -> Vec<impl IntoElement> {
let mut items = Vec::new();
uniform_list(
"relays",
total,
cx.processor(move |_v, range, _window, cx| {
let mut items = Vec::new();
for url in self.relays.iter() {
items.push(
h_flex()
.id(SharedString::from(url.to_string()))
.group("")
.flex_1()
.w_full()
.h_8()
.px_2()
.justify_between()
.rounded(cx.theme().radius)
.bg(cx.theme().secondary_background)
.text_color(cx.theme().secondary_foreground)
.child(div().text_sm().child(SharedString::from(url.to_string())))
.child(
Button::new("remove_{ix}")
.icon(IconName::Close)
.xsmall()
.ghost()
.invisible()
.group_hover("", |this| this.visible())
.on_click({
let url = url.to_owned();
cx.listener(move |this, _ev, _window, cx| {
this.remove(&url, cx);
})
}),
),
)
}
for ix in range {
let Some(url) = relays.iter().nth(ix) else {
continue;
};
items.push(
div()
.id(SharedString::from(url.to_string()))
.group("")
.w_full()
.h_9()
.py_0p5()
.child(
h_flex()
.px_2()
.flex()
.justify_between()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(
div().text_sm().child(SharedString::from(url.to_string())),
)
.child(
Button::new("remove_{ix}")
.icon(IconName::Close)
.xsmall()
.ghost()
.invisible()
.group_hover("", |this| this.visible())
.on_click({
let url = url.to_owned();
cx.listener(move |this, _ev, _window, cx| {
this.remove(&url, cx);
})
}),
),
),
)
}
items
}),
)
.h_full()
items
}
fn render_empty(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.mt_2()
.h_20()
.justify_center()
.border_2()
@@ -282,36 +295,48 @@ impl Focusable for MessagingRelayPanel {
impl Render for MessagingRelayPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.size_full()
.items_center()
.justify_center()
.p_2()
.gap_10()
.p_3()
.gap_2()
.w_full()
.child(
div()
.text_center()
.font_semibold()
.line_height(relative(1.25))
.child(SharedString::from("Update Messaging Relays")),
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from(MSG)),
)
.child(divider(cx))
.child(
v_flex()
.w_112()
.gap_2()
.flex_1()
.w_full()
.text_sm()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Relays:")),
)
.child(
v_flex()
.gap_1p5()
.gap_1()
.child(
h_flex()
.gap_1()
.w_full()
.child(TextInput::new(&self.input).small())
.child(
TextInput::new(&self.input)
.small()
.bordered(false)
.cleanable(),
)
.child(
Button::new("add")
.icon(IconName::Plus)
.label("Add")
.tooltip("Add relay")
.ghost()
.size(rems(2.))
.on_click(cx.listener(move |this, _, window, cx| {
this.add(window, cx);
})),
@@ -328,17 +353,25 @@ impl Render for MessagingRelayPanel {
}),
)
.map(|this| {
if !self.relays.is_empty() {
this.child(self.render_list(window, cx))
} else {
if self.relays.is_empty() {
this.child(self.render_empty(window, cx))
} else {
this.child(
v_flex()
.gap_1()
.flex_1()
.w_full()
.children(self.render_list_items(cx)),
)
}
})
.child(divider(cx))
.child(
Button::new("submit")
.label("Update")
.primary()
.small()
.loading(self.updating)
.disabled(self.updating)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.set_relays(window, cx);
})),