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_group;
|
||||
pub mod profile;
|
||||
pub mod relays;
|
||||
pub mod restore;
|
||||
pub mod screening;
|
||||
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."))
|
||||
}
|
||||
Reference in New Issue
Block a user