chore: replace relay panels with relays dialog
This commit is contained in:
@@ -3,6 +3,7 @@ pub mod join_group;
|
|||||||
pub mod new_chat;
|
pub mod new_chat;
|
||||||
pub mod new_group;
|
pub mod new_group;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
pub mod relays;
|
||||||
pub mod restore;
|
pub mod restore;
|
||||||
pub mod screening;
|
pub mod screening;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|||||||
@@ -0,0 +1,689 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use anyhow::{Error, anyhow};
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
App, AppContext, ClickEvent, Context, Div, Entity, InteractiveElement, IntoElement,
|
||||||
|
ParentElement, Render, SharedString, Stateful, Styled, Subscription, Task, TextAlign,
|
||||||
|
WeakEntity, Window, div, px, rems,
|
||||||
|
};
|
||||||
|
use instant::Duration;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{SmallVec, smallvec};
|
||||||
|
use state::NostrRegistry;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
|
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||||
|
use ui::scroll::ScrollableElement;
|
||||||
|
use ui::tab::Tab;
|
||||||
|
use ui::tab::tab_bar::TabBar;
|
||||||
|
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
|
const GOSSIP_MSG: &str = "Gossip Relays are the relays where you publish all your events and discover events from others.";
|
||||||
|
const INBOX_MSG: &str = "Messaging Relays host your private messages. Others find these relays and send you messages there.";
|
||||||
|
|
||||||
|
pub fn open(window: &mut Window, cx: &mut App) {
|
||||||
|
let view = cx.new(|cx| RelayManager::new(window, cx));
|
||||||
|
|
||||||
|
window.open_dialog(cx, move |this, _window, _cx| {
|
||||||
|
this.title("Relays").child(view.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State of the NIP-65 relay list (gossip relays).
|
||||||
|
struct GossipRelays {
|
||||||
|
/// Relay URL input
|
||||||
|
input: Entity<InputState>,
|
||||||
|
/// Metadata assigned to relays added afterwards
|
||||||
|
metadata: Option<RelayMetadata>,
|
||||||
|
/// All relays
|
||||||
|
relays: HashSet<(RelayUrl, Option<RelayMetadata>)>,
|
||||||
|
/// Whether the relay list is updating
|
||||||
|
updating: bool,
|
||||||
|
error: Option<SharedString>,
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State of the NIP-17 inbox relays (messaging relays).
|
||||||
|
struct InboxRelays {
|
||||||
|
/// Relay URL input
|
||||||
|
input: Entity<InputState>,
|
||||||
|
/// All relays
|
||||||
|
relays: HashSet<RelayUrl>,
|
||||||
|
/// Whether the relays are updating
|
||||||
|
updating: bool,
|
||||||
|
error: Option<SharedString>,
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RelayManager {
|
||||||
|
/// Index of the active tab, `0` for gossip relays and `1` for inbox relays
|
||||||
|
active_tab: usize,
|
||||||
|
gossip: GossipRelays,
|
||||||
|
inbox: InboxRelays,
|
||||||
|
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RelayManager {
|
||||||
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let gossip = GossipRelays::new(window, cx);
|
||||||
|
let inbox = InboxRelays::new(window, cx);
|
||||||
|
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Add a relay when the user presses enter in the gossip input
|
||||||
|
cx.subscribe_in(&gossip.input, window, |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.gossip.add(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Add a relay when the user presses enter in the inbox input
|
||||||
|
cx.subscribe_in(&inbox.input, window, |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.inbox.add(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run at the end of current cycle
|
||||||
|
cx.defer_in(window, |this, window, cx| {
|
||||||
|
this.gossip.load(window, cx);
|
||||||
|
this.inbox.load(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
active_tab: 0,
|
||||||
|
gossip,
|
||||||
|
inbox,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_gossip(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let manager = cx.entity().downgrade();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(GOSSIP_MSG)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.w_full()
|
||||||
|
.child(Input::new(&self.gossip.input).small().cleanable(true))
|
||||||
|
.child(
|
||||||
|
Button::new("metadata")
|
||||||
|
.map(|this| {
|
||||||
|
if let Some(metadata) = self.gossip.metadata {
|
||||||
|
this.label(metadata.to_string())
|
||||||
|
} else {
|
||||||
|
this.label("R & W")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.tooltip("Relay metadata")
|
||||||
|
.ghost()
|
||||||
|
.h(rems(2.))
|
||||||
|
.text_xs()
|
||||||
|
.dropdown_menu(move |menu, _window, _cx| {
|
||||||
|
menu.item(metadata_item(&manager, RelayMetadata::Read))
|
||||||
|
.item(metadata_item(&manager, RelayMetadata::Write))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("add-gossip")
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.tooltip("Add relay")
|
||||||
|
.ghost()
|
||||||
|
.size(rems(2.))
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.gossip.add(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.gossip.error.as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.italic()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_danger)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|this| {
|
||||||
|
if self.gossip.relays.is_empty() {
|
||||||
|
this.child(empty_relays(cx))
|
||||||
|
} else {
|
||||||
|
this.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.w_full()
|
||||||
|
.max_h(rems(16.))
|
||||||
|
.overflow_y_scrollbar()
|
||||||
|
.children(self.gossip_rows(cx)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gossip_rows(&self, cx: &mut Context<Self>) -> Vec<Stateful<Div>> {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
|
for (relay, metadata) in self.gossip.relays.iter() {
|
||||||
|
let url = relay.to_owned();
|
||||||
|
let badge = match metadata {
|
||||||
|
Some(metadata) => SharedString::from(metadata.to_string()),
|
||||||
|
None => SharedString::from("Read and Write"),
|
||||||
|
};
|
||||||
|
|
||||||
|
rows.push(relay_row(
|
||||||
|
url.clone(),
|
||||||
|
Some(badge),
|
||||||
|
cx.listener(move |this, _event, _window, cx| this.gossip.remove(&url, cx)),
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_inbox(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(INBOX_MSG)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.w_full()
|
||||||
|
.child(Input::new(&self.inbox.input).small().cleanable(true))
|
||||||
|
.child(
|
||||||
|
Button::new("add-inbox")
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.tooltip("Add relay")
|
||||||
|
.ghost()
|
||||||
|
.size(rems(2.))
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.inbox.add(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.inbox.error.as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.italic()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_danger)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|this| {
|
||||||
|
if self.inbox.relays.is_empty() {
|
||||||
|
this.child(empty_relays(cx))
|
||||||
|
} else {
|
||||||
|
this.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.w_full()
|
||||||
|
.max_h(rems(16.))
|
||||||
|
.overflow_y_scrollbar()
|
||||||
|
.children(self.inbox_rows(cx)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inbox_rows(&self, cx: &mut Context<Self>) -> Vec<Stateful<Div>> {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
|
for relay in self.inbox.relays.iter() {
|
||||||
|
let url = relay.to_owned();
|
||||||
|
|
||||||
|
rows.push(relay_row(
|
||||||
|
url.clone(),
|
||||||
|
None,
|
||||||
|
cx.listener(move |this, _event, _window, cx| this.inbox.remove(&url, cx)),
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tab_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
TabBar::new("relays-tabs")
|
||||||
|
.segmented(true)
|
||||||
|
.selected_index(self.active_tab)
|
||||||
|
.on_click(cx.listener(|this, ix: &usize, _window, cx| {
|
||||||
|
this.active_tab = *ix;
|
||||||
|
cx.notify();
|
||||||
|
}))
|
||||||
|
.child(Tab::new().label("Gossip Relays"))
|
||||||
|
.child(Tab::new().label("Messaging Relays"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_actions(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let updating = if self.active_tab == 0 {
|
||||||
|
self.gossip.updating
|
||||||
|
} else {
|
||||||
|
self.inbox.updating
|
||||||
|
};
|
||||||
|
|
||||||
|
h_flex().gap_1().justify_end().child(
|
||||||
|
Button::new("update")
|
||||||
|
.icon(IconName::CheckCircle)
|
||||||
|
.label("Update")
|
||||||
|
.primary()
|
||||||
|
.font_semibold()
|
||||||
|
.loading(updating)
|
||||||
|
.disabled(updating)
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
if this.active_tab == 0 {
|
||||||
|
this.gossip.publish(window, cx);
|
||||||
|
} else {
|
||||||
|
this.inbox.publish(window, cx);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GossipRelays {
|
||||||
|
fn new(window: &mut Window, cx: &mut Context<RelayManager>) -> Self {
|
||||||
|
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
metadata: None,
|
||||||
|
relays: HashSet::new(),
|
||||||
|
updating: false,
|
||||||
|
error: None,
|
||||||
|
tasks: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn load(&mut self, window: &mut Window, cx: &mut Context<RelayManager>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let task: Task<Result<Vec<(RelayUrl, Option<RelayMetadata>)>, Error>> = cx
|
||||||
|
.background_spawn(async move {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::RelayList)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if let Some(event) = client.database().query(filter).await?.into_iter().next() {
|
||||||
|
Ok(nip65::extract_relay_list(&event).collect())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Not found."))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let relays = task.await?;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.gossip.relays.extend(relays);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, window: &mut Window, cx: &mut Context<RelayManager>) {
|
||||||
|
let value = self.input.read(cx).value().to_string();
|
||||||
|
let metadata = self.metadata;
|
||||||
|
|
||||||
|
if !value.starts_with("ws") {
|
||||||
|
self.set_error("Relay URl is invalid", window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(url) = RelayUrl::parse(&value) {
|
||||||
|
if self.relays.insert((url, metadata)) {
|
||||||
|
self.input.update(cx, |this, cx| {
|
||||||
|
this.set_value("", window, cx);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.set_error("Relay URl is invalid", window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, url: &RelayUrl, cx: &mut Context<RelayManager>) {
|
||||||
|
self.relays.retain(|(relay, _)| relay != url);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish(&mut self, window: &mut Window, cx: &mut Context<RelayManager>) {
|
||||||
|
if self.relays.is_empty() {
|
||||||
|
self.set_error("You need to add at least 1 relay", window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
|
let relays = self.relays.clone();
|
||||||
|
|
||||||
|
self.set_updating(true, cx);
|
||||||
|
|
||||||
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
|
let event = nip65::RelayList::new(relays)
|
||||||
|
.finalize_async(&signer)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Set relay list for current user
|
||||||
|
client.send_event(&event).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(_) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.gossip.set_updating(false, cx);
|
||||||
|
this.gossip.load(window, cx);
|
||||||
|
|
||||||
|
window.push_notification("Update successful", cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.gossip.set_updating(false, cx);
|
||||||
|
this.gossip.set_error(error.to_string(), window, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<E>(&mut self, error: E, window: &mut Window, cx: &mut Context<RelayManager>)
|
||||||
|
where
|
||||||
|
E: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error = Some(error.into());
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
// Clear the error message after a delay
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.gossip.error = None;
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_updating(&mut self, updating: bool, cx: &mut Context<RelayManager>) {
|
||||||
|
self.updating = updating;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InboxRelays {
|
||||||
|
fn new(window: &mut Window, cx: &mut Context<RelayManager>) -> Self {
|
||||||
|
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
relays: HashSet::new(),
|
||||||
|
updating: false,
|
||||||
|
error: None,
|
||||||
|
tasks: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&mut self, window: &mut Window, cx: &mut Context<RelayManager>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if let Some(event) = client.database().query(filter).await?.into_iter().next() {
|
||||||
|
Ok(nip17::extract_relay_list(&event).collect())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Not found."))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let relays = task.await?;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.inbox.relays.extend(relays);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, window: &mut Window, cx: &mut Context<RelayManager>) {
|
||||||
|
let value = self.input.read(cx).value().to_string();
|
||||||
|
|
||||||
|
if !value.starts_with("ws") {
|
||||||
|
self.set_error("Relay URl is invalid", window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(url) = RelayUrl::parse(&value) {
|
||||||
|
if self.relays.insert(url) {
|
||||||
|
self.input.update(cx, |this, cx| {
|
||||||
|
this.set_value("", window, cx);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.set_error("Relay URl is invalid", window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, url: &RelayUrl, cx: &mut Context<RelayManager>) {
|
||||||
|
self.relays.remove(url);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish(&mut self, window: &mut Window, cx: &mut Context<RelayManager>) {
|
||||||
|
if self.relays.is_empty() {
|
||||||
|
self.set_error("You need to add at least 1 relay", window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
|
let tags: Vec<Tag> = self
|
||||||
|
.relays
|
||||||
|
.iter()
|
||||||
|
.map(|relay| Tag::from(Nip17Tag::Relay(relay.to_owned())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self.set_updating(true, cx);
|
||||||
|
|
||||||
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
|
let event = EventBuilder::new(Kind::InboxRelays, "")
|
||||||
|
.tags(tags)
|
||||||
|
.finalize_async(&signer)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Set messaging relays
|
||||||
|
client.send_event(&event).to_nip65().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(_) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.inbox.set_updating(false, cx);
|
||||||
|
this.inbox.load(window, cx);
|
||||||
|
|
||||||
|
window.push_notification("Update successful", cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.inbox.set_updating(false, cx);
|
||||||
|
this.inbox.set_error(error.to_string(), window, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<E>(&mut self, error: E, window: &mut Window, cx: &mut Context<RelayManager>)
|
||||||
|
where
|
||||||
|
E: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error = Some(error.into());
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
// Clear the error message after a delay
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.inbox.error = None;
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_updating(&mut self, updating: bool, cx: &mut Context<RelayManager>) {
|
||||||
|
self.updating = updating;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for RelayManager {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let content = if self.active_tab == 0 {
|
||||||
|
self.render_gossip(cx).into_any_element()
|
||||||
|
} else {
|
||||||
|
self.render_inbox(cx).into_any_element()
|
||||||
|
};
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.gap_4()
|
||||||
|
.text_sm()
|
||||||
|
.child(self.render_tab_bar(cx))
|
||||||
|
.child(content)
|
||||||
|
.child(self.render_actions(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_item(manager: &WeakEntity<RelayManager>, metadata: RelayMetadata) -> PopupMenuItem {
|
||||||
|
let manager = manager.clone();
|
||||||
|
let label = match metadata {
|
||||||
|
RelayMetadata::Read => "Read",
|
||||||
|
RelayMetadata::Write => "Write",
|
||||||
|
};
|
||||||
|
|
||||||
|
PopupMenuItem::new(label).on_click(move |_, _, cx| {
|
||||||
|
if let Err(error) = manager.update(cx, |this, cx| {
|
||||||
|
this.gossip.metadata = Some(metadata);
|
||||||
|
cx.notify();
|
||||||
|
}) {
|
||||||
|
log::error!("Failed to set relay metadata: {error}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relay_row(
|
||||||
|
url: RelayUrl,
|
||||||
|
badge: Option<SharedString>,
|
||||||
|
on_remove: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
|
cx: &App,
|
||||||
|
) -> Stateful<Div> {
|
||||||
|
let label = SharedString::from(url.to_string());
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(label.clone())
|
||||||
|
.group("")
|
||||||
|
.w_full()
|
||||||
|
.h_8()
|
||||||
|
.px_2()
|
||||||
|
.justify_between()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().surface_background)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.child(label)
|
||||||
|
.when_some(badge, |this, badge| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.p_0p5()
|
||||||
|
.rounded_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_size(px(8.))
|
||||||
|
.child(badge),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("remove")
|
||||||
|
.icon(IconName::Close)
|
||||||
|
.xsmall()
|
||||||
|
.ghost()
|
||||||
|
.invisible()
|
||||||
|
.group_hover("", |this| this.visible())
|
||||||
|
.on_click(on_remove),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_relays(cx: &App) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.h_20()
|
||||||
|
.justify_center()
|
||||||
|
.border_2()
|
||||||
|
.border_dashed()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.rounded(cx.theme().radius_lg)
|
||||||
|
.text_sm()
|
||||||
|
.text_align(TextAlign::Center)
|
||||||
|
.child(SharedString::from("Please add some relays."))
|
||||||
|
}
|
||||||
@@ -22,10 +22,8 @@ use ui::notification::{Notification, NotificationKind};
|
|||||||
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
use crate::dialogs::restore::RestoreEncryption;
|
use crate::dialogs::restore::RestoreEncryption;
|
||||||
use crate::dialogs::{join_group, new_chat, new_group, profile, settings};
|
use crate::dialogs::{join_group, new_chat, new_group, profile, relays, settings};
|
||||||
use crate::panels::{
|
use crate::panels::{backup, browse, contact_list, greeter, inbox, requests, search};
|
||||||
backup, browse, contact_list, greeter, inbox, messaging_relays, relay_list, requests, search,
|
|
||||||
};
|
|
||||||
use crate::sidebar::Sidebar;
|
use crate::sidebar::Sidebar;
|
||||||
|
|
||||||
mod dialogs;
|
mod dialogs;
|
||||||
@@ -50,7 +48,6 @@ enum Command {
|
|||||||
RefreshEncryption,
|
RefreshEncryption,
|
||||||
ResetEncryption,
|
ResetEncryption,
|
||||||
ShowRelayList,
|
ShowRelayList,
|
||||||
ShowMessaging,
|
|
||||||
ShowProfile,
|
ShowProfile,
|
||||||
ShowSettings,
|
ShowSettings,
|
||||||
ShowBackup,
|
ShowBackup,
|
||||||
@@ -325,14 +322,6 @@ impl Workspace {
|
|||||||
Command::ShowBackup => {
|
Command::ShowBackup => {
|
||||||
self.add_panel_to_dock(backup::init(window, cx), DockPlacement::Left, window, cx);
|
self.add_panel_to_dock(backup::init(window, cx), DockPlacement::Left, window, cx);
|
||||||
}
|
}
|
||||||
Command::ShowMessaging => {
|
|
||||||
self.add_panel_to_dock(
|
|
||||||
messaging_relays::init(window, cx),
|
|
||||||
DockPlacement::Left,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Command::RefreshMessagingRelays => {
|
Command::RefreshMessagingRelays => {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
// Trigger a refresh of the chat registry
|
// Trigger a refresh of the chat registry
|
||||||
@@ -341,12 +330,7 @@ impl Workspace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Command::ShowRelayList => {
|
Command::ShowRelayList => {
|
||||||
self.add_panel_to_dock(
|
relays::open(window, cx);
|
||||||
relay_list::init(window, cx),
|
|
||||||
DockPlacement::Right,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Command::RefreshEncryption => {
|
Command::RefreshEncryption => {
|
||||||
let device = DeviceRegistry::global(cx);
|
let device = DeviceRegistry::global(cx);
|
||||||
|
|||||||
@@ -1,372 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
|
||||||
Task, TextAlign, Window, div, rems,
|
|
||||||
};
|
|
||||||
use instant::Duration;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use smallvec::{SmallVec, smallvec};
|
|
||||||
use state::NostrRegistry;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::dock::{Panel, PanelEvent};
|
|
||||||
use ui::input::{Input, InputEvent, InputState};
|
|
||||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MessagingRelayPanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// Relay URL input
|
|
||||||
input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// Whether the panel is updating
|
|
||||||
updating: bool,
|
|
||||||
|
|
||||||
/// Error message
|
|
||||||
error: Option<SharedString>,
|
|
||||||
|
|
||||||
/// All relays
|
|
||||||
relays: HashSet<RelayUrl>,
|
|
||||||
|
|
||||||
/// Event subscriptions
|
|
||||||
_subscriptions: SmallVec<[Subscription; 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 mut subscriptions = smallvec![];
|
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Subscribe to user's input events
|
|
||||||
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
|
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
|
||||||
this.add(window, cx);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).current_user() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::InboxRelays)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.into_iter().next() {
|
|
||||||
Ok(nip17::extract_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>) {
|
|
||||||
let value = self.input.read(cx).value().to_string();
|
|
||||||
|
|
||||||
if !value.starts_with("ws") {
|
|
||||||
self.set_error("Relay URl is invalid", window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(url) = RelayUrl::parse(&value) {
|
|
||||||
if self.relays.insert(url) {
|
|
||||||
self.input.update(cx, |this, cx| {
|
|
||||||
this.set_value("", window, cx);
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.set_error("Relay URl is invalid", window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove(&mut self, url: &RelayUrl, cx: &mut Context<Self>) {
|
|
||||||
self.relays.remove(url);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_error<E>(&mut self, error: E, window: &mut Window, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
E: Into<SharedString>,
|
|
||||||
{
|
|
||||||
self.error = Some(error.into());
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
|
||||||
|
|
||||||
// Clear the error message after a delay
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.error = None;
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let signer = nostr.read(cx).signer();
|
|
||||||
|
|
||||||
// Construct event tags
|
|
||||||
let tags: Vec<Tag> = self
|
|
||||||
.relays
|
|
||||||
.iter()
|
|
||||||
.map(|relay| Tag::from(Nip17Tag::Relay(relay.to_owned())))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Set updating state
|
|
||||||
self.set_updating(true, cx);
|
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
|
||||||
// Construct nip17 event builder
|
|
||||||
let event = EventBuilder::new(Kind::InboxRelays, "")
|
|
||||||
.tags(tags)
|
|
||||||
.finalize_async(&signer)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Set messaging relays
|
|
||||||
client.send_event(&event).to_nip65().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match task.await {
|
|
||||||
Ok(_) => {
|
|
||||||
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_updating(false, cx);
|
|
||||||
this.set_error(e.to_string(), window, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_list_items(&mut self, cx: &mut Context<Self>) -> Vec<impl IntoElement> {
|
|
||||||
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);
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_empty(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.h_20()
|
|
||||||
.justify_center()
|
|
||||||
.border_2()
|
|
||||||
.border_dashed()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.text_sm()
|
|
||||||
.text_align(TextAlign::Center)
|
|
||||||
.child(SharedString::from("Please add some relays."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for MessagingRelayPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for MessagingRelayPanel {}
|
|
||||||
|
|
||||||
impl Focusable for MessagingRelayPanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for MessagingRelayPanel {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.p_3()
|
|
||||||
.gap_3()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(MSG)),
|
|
||||||
)
|
|
||||||
.child(divider(cx))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.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_1()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.w_full()
|
|
||||||
.child(Input::new(&self.input).small().cleanable(true))
|
|
||||||
.child(
|
|
||||||
Button::new("add")
|
|
||||||
.icon(IconName::Plus)
|
|
||||||
.tooltip("Add relay")
|
|
||||||
.ghost()
|
|
||||||
.size(rems(2.))
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.add(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.when_some(self.error.as_ref(), |this, error| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.italic()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_danger)
|
|
||||||
.child(error.clone()),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.map(|this| {
|
|
||||||
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(
|
|
||||||
Button::new("submit")
|
|
||||||
.icon(IconName::CheckCircle)
|
|
||||||
.label("Update")
|
|
||||||
.primary()
|
|
||||||
.font_semibold()
|
|
||||||
.loading(self.updating)
|
|
||||||
.disabled(self.updating)
|
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
|
||||||
this.set_relays(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,5 @@ pub mod browse;
|
|||||||
pub mod contact_list;
|
pub mod contact_list;
|
||||||
pub mod greeter;
|
pub mod greeter;
|
||||||
pub mod inbox;
|
pub mod inbox;
|
||||||
pub mod messaging_relays;
|
|
||||||
pub mod relay_list;
|
|
||||||
pub mod requests;
|
pub mod requests;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
|||||||
@@ -1,441 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
|
||||||
Task, TextAlign, Window, div, px, rems,
|
|
||||||
};
|
|
||||||
use instant::Duration;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use smallvec::{SmallVec, smallvec};
|
|
||||||
use state::NostrRegistry;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::dock::{Panel, PanelEvent};
|
|
||||||
use ui::input::{Input, InputEvent, InputState};
|
|
||||||
use ui::menu::DropdownMenu;
|
|
||||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
|
||||||
|
|
||||||
const MSG: &str = "Relay List (or Gossip Relays) are a set of relays \
|
|
||||||
where you will publish all your events. Others also publish events \
|
|
||||||
related to you here.";
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<RelayListPanel> {
|
|
||||||
cx.new(|cx| RelayListPanel::new(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
|
||||||
#[action(namespace = relay, no_json)]
|
|
||||||
enum SetMetadata {
|
|
||||||
Read,
|
|
||||||
Write,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RelayListPanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// Relay URL input
|
|
||||||
input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// Whether the panel is updating
|
|
||||||
updating: bool,
|
|
||||||
|
|
||||||
/// Relay metadata input
|
|
||||||
metadata: Entity<Option<RelayMetadata>>,
|
|
||||||
|
|
||||||
/// Error message
|
|
||||||
error: Option<SharedString>,
|
|
||||||
|
|
||||||
// All relays
|
|
||||||
relays: HashSet<(RelayUrl, Option<RelayMetadata>)>,
|
|
||||||
|
|
||||||
// Event subscriptions
|
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
|
|
||||||
// Background tasks
|
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RelayListPanel {
|
|
||||||
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 metadata = cx.new(|_| None);
|
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Subscribe to user's input events
|
|
||||||
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
|
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
|
||||||
this.add(window, cx);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Run at the end of current cycle
|
|
||||||
cx.defer_in(window, |this, window, cx| {
|
|
||||||
this.load(window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
name: "Update Relay List".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
input,
|
|
||||||
updating: false,
|
|
||||||
metadata,
|
|
||||||
relays: HashSet::new(),
|
|
||||||
error: None,
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
tasks: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
let Some(public_key) = nostr.read(cx).current_user() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<Vec<(RelayUrl, Option<RelayMetadata>)>, Error>> = cx
|
|
||||||
.background_spawn(async move {
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::RelayList)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.into_iter().next() {
|
|
||||||
Ok(nip65::extract_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>) {
|
|
||||||
let value = self.input.read(cx).value().to_string();
|
|
||||||
let metadata = self.metadata.read(cx);
|
|
||||||
|
|
||||||
if !value.starts_with("ws") {
|
|
||||||
self.set_error("Relay URl is invalid", window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(url) = RelayUrl::parse(&value) {
|
|
||||||
if self.relays.insert((url, metadata.to_owned())) {
|
|
||||||
self.input.update(cx, |this, cx| {
|
|
||||||
this.set_value("", window, cx);
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.set_error("Relay URl is invalid", window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove(&mut self, url: &RelayUrl, cx: &mut Context<Self>) {
|
|
||||||
self.relays.retain(|(relay, _)| relay != url);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_error<E>(&mut self, error: E, window: &mut Window, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
E: Into<SharedString>,
|
|
||||||
{
|
|
||||||
self.error = Some(error.into());
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
|
||||||
|
|
||||||
// Clear the error message after a delay
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.error = None;
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_updating(&mut self, updating: bool, cx: &mut Context<Self>) {
|
|
||||||
self.updating = updating;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_metadata(&mut self, ev: &SetMetadata, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
match ev {
|
|
||||||
SetMetadata::Read => {
|
|
||||||
self.metadata.update(cx, |this, cx| {
|
|
||||||
*this = Some(RelayMetadata::Read);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
SetMetadata::Write => {
|
|
||||||
self.metadata.update(cx, |this, cx| {
|
|
||||||
*this = Some(RelayMetadata::Write);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let signer = nostr.read(cx).signer();
|
|
||||||
|
|
||||||
// Get all relays
|
|
||||||
let relays = self.relays.clone();
|
|
||||||
|
|
||||||
// Set updating state
|
|
||||||
self.set_updating(true, cx);
|
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
|
||||||
let event = nip65::RelayList::new(relays)
|
|
||||||
.finalize_async(&signer)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Set relay list for current user
|
|
||||||
client.send_event(&event).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match task.await {
|
|
||||||
Ok(_) => {
|
|
||||||
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_updating(false, cx);
|
|
||||||
this.set_error(e.to_string(), window, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_list_items(&mut self, cx: &mut Context<Self>) -> Vec<impl IntoElement> {
|
|
||||||
let mut items = Vec::new();
|
|
||||||
|
|
||||||
for (url, metadata) 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(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.child(SharedString::from(url.to_string()))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.p_0p5()
|
|
||||||
.rounded_xs()
|
|
||||||
.font_semibold()
|
|
||||||
.text_size(px(8.))
|
|
||||||
.text_color(cx.theme().secondary_foreground)
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(metadata) = metadata {
|
|
||||||
this.child(SharedString::from(metadata.to_string()))
|
|
||||||
} else {
|
|
||||||
this.child("Read and Write")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_empty(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.h_20()
|
|
||||||
.justify_center()
|
|
||||||
.border_2()
|
|
||||||
.border_dashed()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.text_sm()
|
|
||||||
.text_align(TextAlign::Center)
|
|
||||||
.child(SharedString::from("Please add some relays."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for RelayListPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for RelayListPanel {}
|
|
||||||
|
|
||||||
impl Focusable for RelayListPanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for RelayListPanel {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.on_action(cx.listener(Self::set_metadata))
|
|
||||||
.p_3()
|
|
||||||
.gap_3()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(MSG)),
|
|
||||||
)
|
|
||||||
.child(divider(cx))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.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_1()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.w_full()
|
|
||||||
.child(Input::new(&self.input).small().cleanable(true))
|
|
||||||
.child(
|
|
||||||
Button::new("metadata")
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(metadata) = self.metadata.read(cx) {
|
|
||||||
this.label(metadata.to_string())
|
|
||||||
} else {
|
|
||||||
this.label("R & W")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.tooltip("Relay metadata")
|
|
||||||
.ghost()
|
|
||||||
.h(rems(2.))
|
|
||||||
.text_xs()
|
|
||||||
.dropdown_menu(|this, _window, _cx| {
|
|
||||||
this.menu("Read", Box::new(SetMetadata::Read))
|
|
||||||
.menu("Write", Box::new(SetMetadata::Write))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("add")
|
|
||||||
.icon(IconName::Plus)
|
|
||||||
.tooltip("Add relay")
|
|
||||||
.ghost()
|
|
||||||
.size(rems(2.))
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.add(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.when_some(self.error.as_ref(), |this, error| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.italic()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_danger)
|
|
||||||
.child(error.clone()),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.map(|this| {
|
|
||||||
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(
|
|
||||||
Button::new("submit")
|
|
||||||
.icon(IconName::CheckCircle)
|
|
||||||
.label("Update")
|
|
||||||
.primary()
|
|
||||||
.font_semibold()
|
|
||||||
.loading(self.updating)
|
|
||||||
.disabled(self.updating)
|
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
|
||||||
this.set_relays(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user