feat: relay status viewer (#143)

* add relay status

* .
This commit is contained in:
reya
2025-09-07 14:54:28 +07:00
committed by GitHub
parent e177facef4
commit 71140beb52
15 changed files with 163 additions and 63 deletions

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm6.22-1.53 3.25-3.25a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 1 1-1.06 1.06l-1.97-1.97v6.69a.75.75 0 0 1-1.5 0V9.56l-1.97 1.97a.75.75 0 0 1-1.06-1.06Z" clip-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 397 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M4 4.75A2.75 2.75 0 0 1 6.75 2h10.5A2.75 2.75 0 0 1 20 4.75v7.917a3.9 3.9 0 0 0-4.091.909l-3.75 3.75a2.25 2.25 0 0 0-.659 1.59v2.334c0 .263.045.515.128.75H6.75A2.75 2.75 0 0 1 4 19.25V4.75Z"/>
<path fill="currentColor" fill-rule="evenodd" d="M19.303 15.697a.9.9 0 0 0-1.273 0l-3.53 3.53V20.5h1.273l3.53-3.53a.9.9 0 0 0 0-1.273Zm-2.333-1.06a2.4 2.4 0 1 1 3.394 3.393l-3.75 3.75a.75.75 0 0 1-.53.22H13.75a.75.75 0 0 1-.75-.75v-2.333a.75.75 0 0 1 .22-.53l3.75-3.75Z" clip-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 622 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linejoin="round" stroke-width="1.5" d="m21.76 11.45-8.146-7.535a.75.75 0 0 0-1.26.55V8a.51.51 0 0 1-.504.504C3.765 8.632 1.604 11.92 1.604 20.25c1.47-2.94 2.22-4.679 10.245-4.748a.501.501 0 0 1 .505.498v3.535a.75.75 0 0 0 1.26.55l8.145-7.535a.75.75 0 0 0 0-1.1Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 405 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M3 11a8 8 0 1 1 14.162 5.102l3.368 3.368a.75.75 0 1 1-1.06 1.06l-3.368-3.368A8 8 0 0 1 3 11Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 230 B

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

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 17v2m6-6v6m6-10v10m6-14v14"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M64.12,147.8a4,4,0,0,1-4,4.2H16a8,8,0,0,1-7.8-6.17,8.35,8.35,0,0,1,1.62-6.93A67.79,67.79,0,0,1,37,117.51a40,40,0,1,1,66.46-35.8,3.94,3.94,0,0,1-2.27,4.18A64.08,64.08,0,0,0,64,144C64,145.28,64,146.54,64.12,147.8Zm182-8.91A67.76,67.76,0,0,0,219,117.51a40,40,0,1,0-66.46-35.8,3.94,3.94,0,0,0,2.27,4.18A64.08,64.08,0,0,1,192,144c0,1.28,0,2.54-.12,3.8a4,4,0,0,0,4,4.2H240a8,8,0,0,0,7.8-6.17A8.33,8.33,0,0,0,246.17,138.89Zm-89,43.18a48,48,0,1,0-58.37,0A72.13,72.13,0,0,0,65.07,212,8,8,0,0,0,72,224H184a8,8,0,0,0,6.93-12A72.15,72.15,0,0,0,157.19,182.07Z"></path></svg>

Before

Width:  |  Height:  |  Size: 676 B

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

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 9.02v2.993M12 15h.01M10.277 3.99 3.275 15.998C2.499 17.328 3.458 19 4.998 19h14.004c1.54 0 2.5-1.671 1.723-3.002L13.723 3.99c-.77-1.32-2.677-1.32-3.447 0ZM12.25 15a.25.25 0 1 1-.5 0 .25.25 0 0 1 .5 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 384 B

View File

@@ -3,6 +3,7 @@ use std::sync::Mutex;
use gpui::{actions, App}; use gpui::{actions, App};
actions!(coop, [DarkMode, Settings, Logout, Quit]); actions!(coop, [DarkMode, Settings, Logout, Quit]);
actions!(sidebar, [Reload, RelayStatus]);
pub fn load_embedded_fonts(cx: &App) { pub fn load_embedded_fonts(cx: &App) {
let asset_source = cx.asset_source(); let asset_source = cx.asset_source();

View File

@@ -1313,7 +1313,7 @@ impl ChatSpace {
) )
}) })
.when(!self.has_nip17_relays, |this| { .when(!self.has_nip17_relays, |this| {
this.child(setup_nip17_relay(t!("relays.button_label"))) this.child(setup_nip17_relay(t!("relays.button")))
}) })
.child( .child(
Button::new("user") Button::new("user")

View File

@@ -86,7 +86,6 @@ impl Preferences {
} }
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) { fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
let title = SharedString::new(t!("relays.modal_title"));
let view = setup_relay::init(Kind::InboxRelays, window, cx); let view = setup_relay::init(Kind::InboxRelays, window, cx);
let weak_view = view.downgrade(); let weak_view = view.downgrade();
@@ -94,7 +93,7 @@ impl Preferences {
let weak_view = weak_view.clone(); let weak_view = weak_view.clone();
this.confirm() this.confirm()
.title(title.clone()) .title(shared_t!("relays.modal"))
.child(view.clone()) .child(view.clone())
.button_props(ModalButtonProps::default().ok_text(t!("common.update"))) .button_props(ModalButtonProps::default().ok_text(t!("common.update")))
.on_ok(move |_, window, cx| { .on_ok(move |_, window, cx| {

View File

@@ -45,7 +45,7 @@ where
modal modal
.confirm() .confirm()
.title(shared_t!("relays.modal_title")) .title(shared_t!("relays.modal"))
.child(view.clone()) .child(view.clone())
.button_props(ModalButtonProps::default().ok_text(t!("common.update"))) .button_props(ModalButtonProps::default().ok_text(t!("common.update")))
.on_ok(move |_, window, cx| { .on_ok(move |_, window, cx| {
@@ -299,7 +299,7 @@ impl SetupRelay {
.justify_center() .justify_center()
.text_sm() .text_sm()
.text_align(TextAlign::Center) .text_align(TextAlign::Center)
.child(shared_t!("relays.add_some_relays")) .child(shared_t!("relays.help_text"))
} }
} }
@@ -339,7 +339,7 @@ impl Render for SetupRelay {
.text_xs() .text_xs()
.font_semibold() .font_semibold()
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_muted)
.child(shared_t!("relays.recommended")), .child(shared_t!("common.recommended")),
) )
.child(h_flex().gap_1().children({ .child(h_flex().gap_1().children({
NIP17_RELAYS.iter().map(|&relay| { NIP17_RELAYS.iter().map(|&relay| {

View File

@@ -6,15 +6,15 @@ use anyhow::{anyhow, Error};
use common::debounced_delay::DebouncedDelay; use common::debounced_delay::DebouncedDelay;
use common::display::{ReadableTimestamp, TextUtils}; use common::display::{ReadableTimestamp, TextUtils};
use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS}; use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use global::{nostr_client, UnwrappingStatus}; use global::{css, nostr_client, UnwrappingStatus};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, div, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, Focusable, InteractiveElement, IntoElement, ParentElement, Render, RetainAllImageCache,
Subscription, Task, Window, SharedString, Styled, Subscription, Task, Window,
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::t; use i18n::{shared_t, t};
use itertools::Itertools; use itertools::Itertools;
use list_item::RoomListItem; use list_item::RoomListItem;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -26,8 +26,10 @@ use theme::ActiveTheme;
use ui::button::{Button, ButtonRounded, ButtonVariants}; use ui::button::{Button, ButtonRounded, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::popup_menu::PopupMenu; use ui::popup_menu::{PopupMenu, PopupMenuExt};
use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt}; use ui::{h_flex, v_flex, ContextModal, Icon, IconName, Selectable, Sizable, StyledExt};
use crate::actions::{RelayStatus, Reload};
mod list_item; mod list_item;
@@ -69,17 +71,16 @@ impl Sidebar {
let global_result = cx.new(|_| None); let global_result = cx.new(|_| None);
let cancel_handle = cx.new(|_| None); let cancel_handle = cx.new(|_| None);
let find_input = cx.new(|cx| { let find_input =
InputState::new(window, cx).placeholder(t!("sidebar.find_or_start_conversation")) cx.new(|cx| InputState::new(window, cx).placeholder(t!("sidebar.search_label")));
});
let chats = Registry::global(cx); let registry = Registry::global(cx);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
subscriptions.push(cx.subscribe_in( subscriptions.push(cx.subscribe_in(
&chats, &registry,
window, window,
move |this, _chats, event, _window, cx| { move |this, _, event, _window, cx| {
if let RegistryEvent::NewRequest(kind) = event { if let RegistryEvent::NewRequest(kind) = event {
this.indicator.update(cx, |this, cx| { this.indicator.update(cx, |this, cx| {
*this = Some(kind.to_owned()); *this = Some(kind.to_owned());
@@ -519,6 +520,88 @@ impl Sidebar {
}); });
} }
fn on_reload(&mut self, _ev: &Reload, window: &mut Window, cx: &mut Context<Self>) {
Registry::global(cx).update(cx, |this, cx| {
this.load_rooms(window, cx);
});
window.push_notification(t!("common.refreshed"), cx);
}
fn on_manage(&mut self, _ev: &RelayStatus, window: &mut Window, cx: &mut Context<Self>) {
let task: Task<Result<Vec<Relay>, Error>> = cx.background_spawn(async move {
let client = nostr_client();
let css = css();
let subscription = client.subscription(&css.gift_wrap_sub_id).await;
let mut relays: Vec<Relay> = vec![];
for (url, _filter) in subscription.into_iter() {
relays.push(client.pool().relay(url).await?);
}
Ok(relays)
});
cx.spawn_in(window, async move |this, cx| {
if let Ok(relays) = task.await {
this.update_in(cx, |this, window, cx| {
this.manage_relays(relays, window, cx);
})
.ok();
}
})
.detach();
}
fn manage_relays(&mut self, relays: Vec<Relay>, window: &mut Window, cx: &mut Context<Self>) {
window.open_modal(cx, move |this, _window, cx| {
this.show_close(true)
.overlay_closable(true)
.keyboard(true)
.title(shared_t!("manage_relays.modal"))
.child(v_flex().pb_4().gap_2().children({
let mut items = Vec::with_capacity(relays.len());
for relay in relays.clone().into_iter() {
let url = relay.url().to_string();
let time = relay.stats().connected_at().to_ago();
let connected = relay.is_connected();
items.push(
h_flex()
.h_8()
.px_2()
.justify_between()
.text_xs()
.bg(cx.theme().elevated_surface_background)
.rounded(cx.theme().radius)
.child(
h_flex()
.gap_1()
.font_semibold()
.child(
Icon::new(IconName::Signal)
.small()
.text_color(cx.theme().danger_active)
.when(connected, |this| {
this.text_color(gpui::green().alpha(0.75))
}),
)
.child(url),
)
.child(
div()
.text_right()
.text_color(cx.theme().text_muted)
.child(shared_t!("manage_relays.time", t = time)),
),
);
}
items
}))
});
}
fn list_items( fn list_items(
&self, &self,
rooms: &[Entity<Room>], rooms: &[Entity<Room>],
@@ -610,6 +693,8 @@ impl Render for Sidebar {
} }
v_flex() v_flex()
.on_action(cx.listener(Self::on_reload))
.on_action(cx.listener(Self::on_manage))
.image_cache(self.image_cache.clone()) .image_cache(self.image_cache.clone())
.size_full() .size_full()
.relative() .relative()
@@ -632,7 +717,7 @@ impl Render for Sidebar {
.suffix( .suffix(
Button::new("find") Button::new("find")
.icon(IconName::Search) .icon(IconName::Search)
.tooltip(t!("sidebar.press_enter_to_search")) .tooltip(t!("sidebar.search_tooltip"))
.transparent() .transparent()
.small(), .small(),
), ),
@@ -693,6 +778,31 @@ impl Render for Sidebar {
.on_click(cx.listener(|this, _, _, cx| { .on_click(cx.listener(|this, _, _, cx| {
this.set_filter(RoomKind::default(), cx); this.set_filter(RoomKind::default(), cx);
})), })),
)
.child(
h_flex()
.flex_1()
.w_full()
.justify_end()
.items_center()
.text_xs()
.child(
Button::new("option")
.icon(IconName::Ellipsis)
.xsmall()
.ghost()
.rounded(ButtonRounded::Full)
.popup_menu(move |this, _window, _cx| {
this.menu(
t!("sidebar.reload_menu"),
Box::new(Reload),
)
.menu(
t!("sidebar.status_menu"),
Box::new(RelayStatus),
)
}),
),
), ),
) )
.child( .child(

View File

@@ -14,8 +14,6 @@ pub enum IconName {
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
ArrowUp, ArrowUp,
ArrowUpCircle,
Bell,
CaretUp, CaretUp,
CaretDown, CaretDown,
CaretDownFill, CaretDownFill,
@@ -28,7 +26,6 @@ pub enum IconName {
CloseCircleFill, CloseCircleFill,
Copy, Copy,
Edit, Edit,
EditFill,
Ellipsis, Ellipsis,
Eye, Eye,
EyeOff, EyeOff,
@@ -52,9 +49,8 @@ pub enum IconName {
Reply, Reply,
Report, Report,
Refresh, Refresh,
Forward, Signal,
Search, Search,
SearchFill,
Settings, Settings,
SortAscending, SortAscending,
SortDescending, SortDescending,
@@ -62,8 +58,8 @@ pub enum IconName {
ThumbsDown, ThumbsDown,
ThumbsUp, ThumbsUp,
Upload, Upload,
UsersThreeFill,
OpenUrl, OpenUrl,
Warning,
WindowClose, WindowClose,
WindowMaximize, WindowMaximize,
WindowMinimize, WindowMinimize,
@@ -78,8 +74,6 @@ impl IconName {
Self::ArrowLeft => "icons/arrow-left.svg", Self::ArrowLeft => "icons/arrow-left.svg",
Self::ArrowRight => "icons/arrow-right.svg", Self::ArrowRight => "icons/arrow-right.svg",
Self::ArrowUp => "icons/arrow-up.svg", Self::ArrowUp => "icons/arrow-up.svg",
Self::ArrowUpCircle => "icons/arrow-up-circle.svg",
Self::Bell => "icons/bell.svg",
Self::CaretRight => "icons/caret-right.svg", Self::CaretRight => "icons/caret-right.svg",
Self::CaretUp => "icons/caret-up.svg", Self::CaretUp => "icons/caret-up.svg",
Self::CaretDown => "icons/caret-down.svg", Self::CaretDown => "icons/caret-down.svg",
@@ -92,7 +86,6 @@ impl IconName {
Self::CloseCircleFill => "icons/close-circle-fill.svg", Self::CloseCircleFill => "icons/close-circle-fill.svg",
Self::Copy => "icons/copy.svg", Self::Copy => "icons/copy.svg",
Self::Edit => "icons/edit.svg", Self::Edit => "icons/edit.svg",
Self::EditFill => "icons/edit-fill.svg",
Self::Ellipsis => "icons/ellipsis.svg", Self::Ellipsis => "icons/ellipsis.svg",
Self::Eye => "icons/eye.svg", Self::Eye => "icons/eye.svg",
Self::EmojiFill => "icons/emoji-fill.svg", Self::EmojiFill => "icons/emoji-fill.svg",
@@ -116,9 +109,8 @@ impl IconName {
Self::Reply => "icons/reply.svg", Self::Reply => "icons/reply.svg",
Self::Report => "icons/report.svg", Self::Report => "icons/report.svg",
Self::Refresh => "icons/refresh.svg", Self::Refresh => "icons/refresh.svg",
Self::Forward => "icons/forward.svg", Self::Signal => "icons/signal.svg",
Self::Search => "icons/search.svg", Self::Search => "icons/search.svg",
Self::SearchFill => "icons/search-fill.svg",
Self::Settings => "icons/settings.svg", Self::Settings => "icons/settings.svg",
Self::SortAscending => "icons/sort-ascending.svg", Self::SortAscending => "icons/sort-ascending.svg",
Self::SortDescending => "icons/sort-descending.svg", Self::SortDescending => "icons/sort-descending.svg",
@@ -126,8 +118,8 @@ impl IconName {
Self::ThumbsDown => "icons/thumbs-down.svg", Self::ThumbsDown => "icons/thumbs-down.svg",
Self::ThumbsUp => "icons/thumbs-up.svg", Self::ThumbsUp => "icons/thumbs-up.svg",
Self::Upload => "icons/upload.svg", Self::Upload => "icons/upload.svg",
Self::UsersThreeFill => "icons/users-three-fill.svg",
Self::OpenUrl => "icons/open-url.svg", Self::OpenUrl => "icons/open-url.svg",
Self::Warning => "icons/warning.svg",
Self::WindowClose => "icons/window-close.svg", Self::WindowClose => "icons/window-close.svg",
Self::WindowMaximize => "icons/window-maximize.svg", Self::WindowMaximize => "icons/window-maximize.svg",
Self::WindowMinimize => "icons/window-minimize.svg", Self::WindowMinimize => "icons/window-minimize.svg",

View File

@@ -30,14 +30,10 @@ pub enum NotificationType {
impl NotificationType { impl NotificationType {
fn icon(&self, cx: &App) -> Icon { fn icon(&self, cx: &App) -> Icon {
match self { match self {
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().element_active), Self::Info => Icon::new(IconName::Info).text_color(cx.theme().element_foreground),
Self::Warning => Icon::new(IconName::Report).text_color(cx.theme().warning_foreground), Self::Success => Icon::new(IconName::Info).text_color(cx.theme().secondary_foreground),
Self::Success => { Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_foreground),
Icon::new(IconName::CheckCircle).text_color(cx.theme().element_foreground) Self::Error => Icon::new(IconName::Warning).text_color(cx.theme().danger_foreground),
}
Self::Error => {
Icon::new(IconName::CloseCircle).text_color(cx.theme().danger_foreground)
}
} }
} }
} }

View File

@@ -45,6 +45,10 @@ common:
en: "Ignore" en: "Ignore"
relay: relay:
en: "Relay" en: "Relay"
relay_invalid:
en: "Relay URL is not valid."
recommended:
en: "Recommended:"
auto_update: auto_update:
updating: updating:
@@ -167,20 +171,22 @@ login:
en: "Logging in..." en: "Logging in..."
relays: relays:
button_label: button:
en: "Configure the Messaging Relays to receive messages" en: "Configure the Messaging Relays to receive messages"
modal_title: modal:
en: "Set Up Messaging Relays" en: "Set Up Messaging Relays"
description: description:
en: "In order to receive messages from others, you need to set up at least one Messaging Relay." en: "In order to receive messages from others, you need to set up at least one Messaging Relay."
add_some_relays: help_text:
en: "Please add some relays." en: "Please add some relays."
invalid:
en: "Relay URL is not valid."
empty: empty:
en: "You need to add at least 1 relay to receive messages." en: "You need to add at least 1 relay to receive messages from others."
recommended:
en: "Recommended:" manage_relays:
modal:
en: "Messaging Relay Status"
time:
en: "Last activity: %{t}"
subject: subject:
title: title:
@@ -353,9 +359,13 @@ chat:
en: "%{u} has not set up Messaging Relays, so they won't receive your message." en: "%{u} has not set up Messaging Relays, so they won't receive your message."
sidebar: sidebar:
find_or_start_conversation: reload_menu:
en: "Reload"
status_menu:
en: "Relay Status"
search_label:
en: "Find or start a conversation" en: "Find or start a conversation"
press_enter_to_search: search_tooltip:
en: "Press Enter to search" en: "Press Enter to search"
empty: empty:
en: "There are no users matching query %{query}" en: "There are no users matching query %{query}"