This commit is contained in:
2026-03-10 09:20:13 +07:00
parent 12a0e6db08
commit 2b2ff135ba
9 changed files with 238 additions and 45 deletions

View File

@@ -282,7 +282,7 @@ impl ChatRegistry {
}
/// Get all messages for current user
fn get_messages(&mut self, cx: &mut Context<Self>) {
pub fn get_messages(&mut self, cx: &mut Context<Self>) {
let task = self.subscribe(cx);
self.tasks.push(cx.spawn(async move |this, cx| {

View File

@@ -604,7 +604,10 @@ impl ChatPanel {
Err(e) => {
this.update_in(cx, |this, window, cx| {
this.set_uploading(false, cx);
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
})?;
}
}
@@ -652,7 +655,10 @@ impl ChatPanel {
})
.is_err()
{
window.push_notification(Notification::error("Failed to change subject"), cx);
window.push_notification(
Notification::error("Failed to change subject").autohide(false),
cx,
);
}
}
Command::ChangeSigner(kind) => {
@@ -663,7 +669,10 @@ impl ChatPanel {
})
.is_err()
{
window.push_notification(Notification::error("Failed to change signer"), cx);
window.push_notification(
Notification::error("Failed to change signer").autohide(false),
cx,
);
}
}
Command::ToggleBackup => {
@@ -674,7 +683,10 @@ impl ChatPanel {
})
.is_err()
{
window.push_notification(Notification::error("Failed to toggle backup"), cx);
window.push_notification(
Notification::error("Failed to toggle backup").autohide(false),
cx,
);
}
}
Command::Subject => {

View File

@@ -1,7 +1,7 @@
use gpui::http_client::Url;
use gpui::{
div, px, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
Styled, Window,
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Window, div, px,
};
use settings::{AppSettings, AuthMode};
use theme::{ActiveTheme, ThemeMode};
@@ -11,7 +11,7 @@ use ui::input::{InputState, TextInput};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::Notification;
use ui::switch::Switch;
use ui::{h_flex, v_flex, IconName, Sizable, WindowExtension};
use ui::{IconName, Sizable, WindowExtension, h_flex, v_flex};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
cx.new(|cx| Preferences::new(window, cx))
@@ -41,7 +41,7 @@ impl Preferences {
AppSettings::update_file_server(url, cx);
}
Err(e) => {
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(Notification::error(e.to_string()).autohide(false), cx);
}
}
}

View File

@@ -3,21 +3,21 @@ use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error};
use gpui::{
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
Window,
Window, div,
};
use nostr_sdk::prelude::*;
use person::{shorten_pubkey, Person, PersonRegistry};
use person::{Person, PersonRegistry, shorten_pubkey};
use settings::AppSettings;
use state::{upload, NostrRegistry};
use state::{NostrRegistry, upload};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::input::{InputState, TextInput};
use ui::notification::Notification;
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
cx.new(|cx| ProfilePanel::new(public_key, window, cx))
@@ -186,7 +186,10 @@ impl ProfilePanel {
Err(e) => {
this.update_in(cx, |this, window, cx| {
this.set_uploading(false, cx);
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
})?;
}
}
@@ -269,7 +272,10 @@ impl ProfilePanel {
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
})?;
}
};

View File

@@ -180,7 +180,10 @@ impl Sidebar {
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
})?;
}
};

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
use ::settings::AppSettings;
use chat::{ChatEvent, ChatRegistry};
use device::DeviceRegistry;
use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder;
use gpui::{
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
@@ -39,6 +39,8 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
cx.new(|cx| Workspace::new(window, cx))
}
struct RelayNotifcation;
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = workspace, no_json)]
enum Command {
@@ -65,15 +67,23 @@ pub struct Workspace {
/// App's Dock Area
dock: Entity<DockArea>,
/// Whether a user's relay list is connected
relay_connected: bool,
/// Whether the inbox is connected
inbox_connected: bool,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 4]>,
_subscriptions: SmallVec<[Subscription; 6]>,
}
impl Workspace {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
let device = DeviceRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let npubs = nostr.read(cx).npubs();
let chat = ChatRegistry::global(cx);
let titlebar = cx.new(|_| TitleBar::new());
let dock = cx.new(|cx| DockArea::new(window, cx));
@@ -101,6 +111,7 @@ impl Workspace {
match event {
StateEvent::Connecting => {
let note = Notification::new()
.id::<RelayNotifcation>()
.message("Connecting to the bootstrap relay...")
.title("Relays")
.icon(IconName::Relay);
@@ -109,6 +120,7 @@ impl Workspace {
}
StateEvent::Connected => {
let note = Notification::new()
.id::<RelayNotifcation>()
.message("Connected to the bootstrap relay")
.title("Relays")
.with_kind(NotificationKind::Success)
@@ -120,22 +132,36 @@ impl Workspace {
this.relay_notification(window, cx);
}
StateEvent::RelayConnected => {
let note = Notification::new()
.message("Connected to user's relay list")
.title("Relays")
.with_kind(NotificationKind::Success)
.icon(IconName::Relay);
window.push_notification(note, cx);
window.clear_notification::<RelayNotifcation>(cx);
this.set_relay_connected(true, cx);
}
StateEvent::SignerSet => {
this.set_center_layout(window, cx);
this.set_relay_connected(false, cx);
this.set_inbox_connected(false, cx);
}
_ => {}
};
}),
);
subscriptions.push(
// Observe all events emitted by the device registry
cx.subscribe_in(&device, window, |_this, _device, ev, window, cx| {
match ev {
DeviceEvent::Set => {
window.push_notification(
Notification::success("Encryption Key has been set"),
cx,
);
}
DeviceEvent::Error(error) => {
window.push_notification(Notification::error(error).autohide(false), cx);
}
};
}),
);
subscriptions.push(
// Observe all events emitted by the chat registry
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
@@ -164,6 +190,12 @@ impl Workspace {
});
});
}
ChatEvent::Subscribed => {
this.set_inbox_connected(true, cx);
}
ChatEvent::Error(error) => {
window.push_notification(Notification::error(error).autohide(false), cx);
}
_ => {}
};
}),
@@ -188,6 +220,8 @@ impl Workspace {
Self {
titlebar,
dock,
relay_connected: false,
inbox_connected: false,
_subscriptions: subscriptions,
}
}
@@ -219,6 +253,18 @@ impl Workspace {
.collect()
}
/// Set whether the relay list is connected
fn set_relay_connected(&mut self, connected: bool, cx: &mut Context<Self>) {
self.relay_connected = connected;
cx.notify();
}
/// Set whether the inbox is connected
fn set_inbox_connected(&mut self, connected: bool, cx: &mut Context<Self>) {
self.inbox_connected = connected;
cx.notify();
}
/// Set the dock layout
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
@@ -301,6 +347,12 @@ impl Workspace {
);
});
}
Command::RefreshMessagingRelays => {
let chat = ChatRegistry::global(cx);
chat.update(cx, |this, cx| {
this.get_messages(cx);
});
}
Command::ShowRelayList => {
self.dock.update(cx, |this, cx| {
this.add_panel(
@@ -311,6 +363,16 @@ impl Workspace {
);
});
}
Command::RefreshRelayList => {
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
if let Some(public_key) = signer.public_key() {
nostr.update(cx, |this, cx| {
this.ensure_relay_list(&public_key, cx);
});
}
}
Command::RefreshEncryption => {
let device = DeviceRegistry::global(cx);
device.update(cx, |this, cx| {
@@ -326,7 +388,6 @@ impl Workspace {
Command::ToggleAccount => {
self.account_selector(window, cx);
}
_ => {}
}
}
@@ -364,8 +425,10 @@ impl Workspace {
window.close_modal(cx);
}
Err(e) => {
window
.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
}
})
.ok();
@@ -489,6 +552,7 @@ impl Workspace {
let note = Notification::new()
.autohide(false)
.id::<RelayNotifcation>()
.icon(IconName::Relay)
.title("Gossip Relays are required")
.content(move |_this, _window, cx| {
@@ -607,6 +671,16 @@ impl Workspace {
}
fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let relay_connected = self.relay_connected;
let inbox_connected = self.inbox_connected;
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return div();
};
h_flex()
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
.gap_3()
@@ -621,7 +695,7 @@ impl Workspace {
let state = device.read(cx).state();
this.min_w(px(260.))
.item(PopupMenuItem::element(move |_window, _cx| {
.item(PopupMenuItem::element(move |_window, cx| {
h_flex()
.px_1()
.w_full()
@@ -633,7 +707,7 @@ impl Workspace {
.rounded_full()
.when(state.set(), |this| this.bg(gpui::green()))
.when(state.requesting(), |this| {
this.bg(gpui::yellow())
this.bg(cx.theme().icon_accent)
}),
)
.child(SharedString::from(state.to_string()))
@@ -651,6 +725,83 @@ impl Workspace {
)
}),
)
.child(
Button::new("inbox")
.icon(IconName::Inbox)
.small()
.ghost()
.loading(!inbox_connected)
.disabled(!inbox_connected)
.when(!inbox_connected, |this| {
this.tooltip("Connecting to user's messaging relays...")
})
.when(inbox_connected, |this| this.indicator())
.dropdown_menu(move |this, _window, cx| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
let urls: Vec<SharedString> = profile
.messaging_relays()
.iter()
.map(|url| SharedString::from(url.to_string()))
.collect();
// Header
let menu = this.min_w(px(260.)).label("Messaging Relays");
// Content
let menu = urls.into_iter().fold(menu, |this, url| {
this.item(PopupMenuItem::element(move |_window, _cx| {
h_flex()
.px_1()
.w_full()
.gap_2()
.text_sm()
.child(div().size_1p5().rounded_full().bg(gpui::green()))
.child(url.clone())
}))
});
// Footer
menu.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshMessagingRelays),
)
.menu_with_icon(
"Update relays",
IconName::Settings,
Box::new(Command::ShowMessaging),
)
}),
)
.child(
Button::new("relay-list")
.icon(IconName::Relay)
.small()
.ghost()
.loading(!relay_connected)
.disabled(!relay_connected)
.when(!relay_connected, |this| {
this.tooltip("Connecting to user's relay list...")
})
.when(relay_connected, |this| this.indicator())
.dropdown_menu(move |this, _window, _cx| {
this.label("User's Relay List")
.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshRelayList),
)
.menu_with_icon(
"Update",
IconName::Settings,
Box::new(Command::ShowRelayList),
)
}),
)
}
}

View File

@@ -597,7 +597,10 @@ impl DeviceRegistry {
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
})
.ok();
}

View File

@@ -288,7 +288,10 @@ impl RelayAuth {
);
}
Err(e) => {
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
}
}
})

View File

@@ -29,9 +29,13 @@ impl NotificationKind {
fn icon(&self, cx: &App) -> Icon {
match self {
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().icon),
Self::Success => Icon::new(IconName::CheckCircle).text_color(cx.theme().icon_accent),
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_active),
Self::Error => Icon::new(IconName::CloseCircle).text_color(cx.theme().danger_active),
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_foreground),
Self::Success => {
Icon::new(IconName::CheckCircle).text_color(cx.theme().secondary_foreground)
}
Self::Error => {
Icon::new(IconName::CloseCircle).text_color(cx.theme().danger_foreground)
}
}
}
}
@@ -282,6 +286,9 @@ impl Styled for Notification {
}
impl Render for Notification {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let closing = self.closing;
let placement = cx.theme().notification.placement;
let content = self
.content_builder
.clone()
@@ -297,9 +304,15 @@ impl Render for Notification {
Some(kind) => Some(kind.icon(cx)),
};
let has_icon = icon.is_some();
let closing = self.closing;
let placement = cx.theme().notification.placement;
let background = match self.kind {
Some(NotificationKind::Error) => cx.theme().danger_background,
_ => cx.theme().surface_background,
};
let text_color = match self.kind {
Some(NotificationKind::Error) => cx.theme().danger_foreground,
_ => cx.theme().text,
};
h_flex()
.id("notification")
@@ -309,7 +322,8 @@ impl Render for Notification {
.w_112()
.border_1()
.border_color(cx.theme().border)
.bg(cx.theme().surface_background)
.bg(background)
.text_color(text_color)
.rounded(cx.theme().radius_lg)
.when(cx.theme().shadow, |this| this.shadow_md())
.p_2()
@@ -318,22 +332,23 @@ impl Render for Notification {
.items_start()
.refine_style(&self.style)
.when_some(icon, |this, icon| {
this.child(div().flex_shrink_0().pt_1().child(icon))
this.child(div().flex_shrink_0().pt(px(3.)).child(icon))
})
.child(
v_flex()
.flex_1()
.overflow_hidden()
.when(has_icon, |this| this.pl_1())
.when_some(self.title.clone(), |this, title| {
this.child(div().text_sm().font_semibold().child(title))
})
.when_some(self.message.clone(), |this, message| {
this.child(div().text_sm().child(message))
})
.when_some(content, |this, content| this.child(content)),
.when_some(content, |this, content| this.child(content))
.when_some(action, |this, action| {
this.child(h_flex().flex_1().gap_1().justify_end().child(action))
}),
)
.when_some(action, |this, action| this.child(action))
.child(
div()
.absolute()