diff --git a/crates/workspace/src/dialogs/mod.rs b/crates/workspace/src/dialogs/mod.rs index 0dbfcca0..43495b33 100644 --- a/crates/workspace/src/dialogs/mod.rs +++ b/crates/workspace/src/dialogs/mod.rs @@ -1,3 +1,5 @@ pub mod import; +pub mod new_chat; +pub mod new_community; pub mod restore; pub mod settings; diff --git a/crates/workspace/src/dialogs/new_chat.rs b/crates/workspace/src/dialogs/new_chat.rs new file mode 100644 index 00000000..96710dc3 --- /dev/null +++ b/crates/workspace/src/dialogs/new_chat.rs @@ -0,0 +1,118 @@ +use chat::{ChatRegistry, Room, RoomKind}; +use gpui::prelude::FluentBuilder; +use gpui::{ + App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, + Subscription, Window, div, px, +}; +use nostr_sdk::prelude::*; +use state::NostrRegistry; +use theme::ActiveTheme; +use ui::button::{Button, ButtonVariants}; +use ui::input::{Input, InputEvent, InputState}; +use ui::{StyledExt, WindowExtension, v_flex}; + +pub fn open(window: &mut Window, cx: &mut App) { + let view = cx.new(|cx| NewChat::new(window, cx)); + + window.open_modal(cx, move |this, _window, _cx| { + this.width(px(420.)).title("New chat").child(view.clone()) + }); +} + +pub struct NewChat { + /// Public key input + input: Entity, + + /// Error message + error: Option, + + /// Input subscription + _subscription: Option, +} + +impl NewChat { + fn new(window: &mut Window, cx: &mut Context) -> Self { + let input = cx.new(|cx| InputState::new(window, cx).placeholder("npub")); + + let subscription = cx.subscribe_in(&input, window, |this, _input, event, window, cx| { + if let InputEvent::PressEnter { .. } = event { + this.start_chat(window, cx); + } + }); + + Self { + input, + error: None, + _subscription: Some(subscription), + } + } + + fn start_chat(&mut self, window: &mut Window, cx: &mut Context) { + let value = self.input.read(cx).value().to_string(); + + let Ok(peer) = PublicKey::parse(&value) else { + self.set_error("Public key is invalid", cx); + return; + }; + + let nostr = NostrRegistry::global(cx); + let Some(current_user) = nostr.read(cx).current_user() else { + self.set_error("You are not signed in", cx); + return; + }; + + if peer == current_user { + self.set_error("You cannot chat with yourself", cx); + return; + } + + let room = Room::new(current_user, [peer]) + .organize(¤t_user) + .kind(RoomKind::Ongoing); + + let chat = ChatRegistry::global(cx); + chat.update(cx, |chat, cx| { + let room = cx.new(|_| room); + chat.emit_room(&room, window, cx); + }); + + window.close_modal(cx); + } + + fn set_error(&mut self, message: impl Into, cx: &mut Context) { + self.error = Some(message.into()); + cx.notify(); + } +} + +impl Render for NewChat { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .gap_4() + .child( + v_flex() + .gap_1() + .text_color(cx.theme().text_muted) + .child("Public key of the person you want to chat with") + .child(Input::new(&self.input)), + ) + .child( + Button::new("start-chat") + .label("Start chat") + .primary() + .font_semibold() + .on_click(cx.listener(|this, _event, window, cx| { + this.start_chat(window, cx); + })), + ) + .when_some(self.error.clone(), |this, error| { + this.child( + div() + .text_xs() + .text_center() + .text_color(cx.theme().text_danger) + .child(error), + ) + }) + } +} diff --git a/crates/workspace/src/dialogs/new_community.rs b/crates/workspace/src/dialogs/new_community.rs new file mode 100644 index 00000000..4b4f36ba --- /dev/null +++ b/crates/workspace/src/dialogs/new_community.rs @@ -0,0 +1,34 @@ +use community::{CommunityMetadata, CommunityRegistry}; +use gpui::{App, AppContext, ParentElement, Window, px}; +use ui::WindowExtension; +use ui::input::{Input, InputState}; + +pub fn open(window: &mut Window, cx: &mut App) { + let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Community name")); + + window.open_modal(cx, move |this, _window, _cx| { + let name_input = name_input.clone(); + + this.width(px(380.)) + .confirm() + .title("New community") + .child(Input::new(&name_input)) + .on_ok(move |_event, _window, cx| { + let name = name_input.read(cx).value().trim().to_owned(); + + if name.is_empty() { + return false; + } + + let metadata = CommunityMetadata { + name, + ..CommunityMetadata::default() + }; + + CommunityRegistry::global(cx) + .update(cx, |registry, cx| registry.create(metadata, cx)); + + true + }) + }); +} diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 97ced488..4e5ef190 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -26,7 +26,7 @@ use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; use crate::dialogs::import::ImportIdentity; use crate::dialogs::restore::RestoreEncryption; -use crate::dialogs::settings; +use crate::dialogs::{new_chat, new_community, settings}; use crate::panels::{ backup, browse, contact_list, greeter, inbox, messaging_relays, profile, relay_list, requests, search, @@ -64,6 +64,8 @@ enum Command { ShowRequests, ShowBrowse, ShowSearch, + NewChat, + NewCommunity, } pub struct Workspace { @@ -324,6 +326,12 @@ impl Workspace { Command::ShowSearch => { self.add_panel_to_dock(search::init(window, cx), DockPlacement::Center, window, cx); } + Command::NewChat => { + new_chat::open(window, cx); + } + Command::NewCommunity => { + new_community::open(window, cx); + } Command::ShowBackup => { self.add_panel_to_dock(backup::init(window, cx), DockPlacement::Left, window, cx); } diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 39da4b7c..b38bfeab 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -22,10 +22,11 @@ use ui::dock::{Panel, PanelEvent}; use ui::indicator::Indicator; use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::nav_item::NavItem; +use ui::notification::Notification; use ui::scroll::Scrollbar; use ui::{ - Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, h_flex, title_bar_drag_handlers, - v_flex, + Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, WindowExtension, h_flex, + title_bar_drag_handlers, v_flex, }; use crate::Command; @@ -62,13 +63,16 @@ impl Sidebar { }), ); - subscriptions.push( - cx.subscribe(&communities, |_this, _communities, event, _cx| { + subscriptions.push(cx.subscribe_in( + &communities, + window, + |_this, _communities, event, window, cx| { if let CommunityEvent::Error(error) = event { - log::error!("community: {error}"); + window + .push_notification(Notification::error(error.clone()).autohide(false), cx); } - }), - ); + }, + )); Self { focus_handle: cx.focus_handle(), @@ -456,7 +460,7 @@ impl Render for Sidebar { .border_r_1() .border_color(cx.theme().border_variant) .child(self.render_user(window, cx)) - .when(active_tab == SidebarTab::Chats, |this| { + .when(active_tab.chat(), |this| { this.child( v_flex() .px_2() @@ -492,17 +496,44 @@ impl Render for Sidebar { window.dispatch_action(Box::new(Command::ShowRequests), cx); } }), + ) + .child( + NavItem::new( + "nav-new-chat", + "New chat", + Icon::new(IconName::Plus).small(), + ) + .on_click(|_event, window, cx| { + window.dispatch_action(Box::new(Command::NewChat), cx) + }), ), ) }) - .when(active_tab == SidebarTab::Communities, |this| { + .when(active_tab.community(), |this| { this.child( - v_flex().px_2().gap_1().child( - NavItem::new("nav-browse", "Browse", Icon::new(IconName::Compass).small()) + v_flex() + .px_2() + .gap_1() + .child( + NavItem::new( + "nav-browse", + "Browse", + Icon::new(IconName::Compass).small(), + ) .on_click(|_event, window, cx| { window.dispatch_action(Box::new(Command::ShowBrowse), cx) }), - ), + ) + .child( + NavItem::new( + "nav-new-community", + "New community", + Icon::new(IconName::Plus).small(), + ) + .on_click(|_event, window, cx| { + window.dispatch_action(Box::new(Command::NewCommunity), cx) + }), + ), ) }) .child( diff --git a/crates/workspace/src/sidebar/tab.rs b/crates/workspace/src/sidebar/tab.rs index 1ce5ecc4..662d3b28 100644 --- a/crates/workspace/src/sidebar/tab.rs +++ b/crates/workspace/src/sidebar/tab.rs @@ -47,6 +47,18 @@ impl SidebarTab { Self::Communities => 2, } } + + pub fn recents(self) -> bool { + matches!(self, Self::Recents) + } + + pub fn chat(self) -> bool { + matches!(self, Self::Chats) + } + + pub fn community(self) -> bool { + matches!(self, Self::Communities) + } } #[derive(IntoElement)]