diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 355ad01..10528a7 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -282,7 +282,7 @@ impl ChatRegistry { } /// Get all messages for current user - fn get_messages(&mut self, cx: &mut Context) { + pub fn get_messages(&mut self, cx: &mut Context) { let task = self.subscribe(cx); self.tasks.push(cx.spawn(async move |this, cx| { diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index eb0efd6..227d874 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -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 => { diff --git a/crates/coop/src/dialogs/settings.rs b/crates/coop/src/dialogs/settings.rs index 75c007f..1e6308a 100644 --- a/crates/coop/src/dialogs/settings.rs +++ b/crates/coop/src/dialogs/settings.rs @@ -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 { 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); } } } diff --git a/crates/coop/src/panels/profile.rs b/crates/coop/src/panels/profile.rs index b320eec..fa26822 100644 --- a/crates/coop/src/panels/profile.rs +++ b/crates/coop/src/panels/profile.rs @@ -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 { 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, + ); })?; } }; diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index bb435ef..0c9bd78 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -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, + ); })?; } }; diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 0731250..c663669 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -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 { 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, + /// 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 { + 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::() .message("Connecting to the bootstrap relay...") .title("Relays") .icon(IconName::Relay); @@ -109,6 +120,7 @@ impl Workspace { } StateEvent::Connected => { let note = Notification::new() + .id::() .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::(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.relay_connected = connected; + cx.notify(); + } + + /// Set whether the inbox is connected + fn set_inbox_connected(&mut self, connected: bool, cx: &mut Context) { + self.inbox_connected = connected; + cx.notify(); + } + /// Set the dock layout fn set_layout(&mut self, window: &mut Window, cx: &mut Context) { 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::() .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) -> 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 = 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), + ) + }), + ) } } diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index a35dd3e..f98f92f 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -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(); } diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index cc0276d..a422565 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -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, + ); } } }) diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index 4829d50..02ddded 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -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) -> 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()