From 0be73e8e827bad0678578a4c3615606305483058 Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 16 Dec 2025 09:18:23 +0700 Subject: [PATCH] feat: simple poc --- assets/icons/ellipsis.svg | 4 + assets/icons/panel-left.svg | 3 + crates/common/src/lib.rs | 2 + crates/common/src/utils.rs | 11 ++ crates/lume/src/panels/feed.rs | 180 +++++++++++++++++++++++++++++++++ crates/lume/src/panels/mod.rs | 1 + crates/lume/src/sidebar/mod.rs | 16 ++- crates/lume/src/workspace.rs | 46 ++++++--- 8 files changed, 239 insertions(+), 24 deletions(-) create mode 100644 assets/icons/ellipsis.svg create mode 100644 assets/icons/panel-left.svg create mode 100644 crates/common/src/utils.rs create mode 100644 crates/lume/src/panels/feed.rs diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg new file mode 100644 index 00000000..f3113af9 --- /dev/null +++ b/assets/icons/ellipsis.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/panel-left.svg b/assets/icons/panel-left.svg new file mode 100644 index 00000000..def688f3 --- /dev/null +++ b/assets/icons/panel-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 56b5b4af..829c4676 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,5 +1,7 @@ pub use constants::*; pub use paths::*; +pub use utils::*; mod constants; mod paths; +mod utils; diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs new file mode 100644 index 00000000..b15a4582 --- /dev/null +++ b/crates/common/src/utils.rs @@ -0,0 +1,11 @@ +use nostr_sdk::prelude::*; + +pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String { + let Ok(pubkey) = public_key.to_bech32(); + + format!( + "{}:{}", + &pubkey[0..(len + 1)], + &pubkey[pubkey.len() - len..] + ) +} diff --git a/crates/lume/src/panels/feed.rs b/crates/lume/src/panels/feed.rs new file mode 100644 index 00000000..0d91aa27 --- /dev/null +++ b/crates/lume/src/panels/feed.rs @@ -0,0 +1,180 @@ +use std::time::Duration; + +use anyhow::Error; +use gpui::prelude::FluentBuilder; +use gpui::{ + div, img, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, + ParentElement, Render, SharedString, Styled, Task, Window, +}; +use gpui_component::dock::{Panel, PanelEvent}; +use gpui_component::{h_flex, v_flex, ActiveTheme}; +use nostr_sdk::prelude::*; +use person::PersonRegistry; +use smallvec::{smallvec, SmallVec}; +use state::client; + +/// Feed +pub struct Feed { + focus_handle: FocusHandle, + + /// All notes that match the query + notes: Entity>, + + /// Public Key + public_key: Option, + + /// Relay Url + relay_url: Option, + + /// Async operations + _tasks: SmallVec<[Task>; 1]>, +} + +impl Feed { + pub fn new( + public_key: Option, + relay_url: Option, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let notes = cx.new(|_| None); + let async_url = relay_url.clone(); + let mut tasks = smallvec![]; + + tasks.push( + // Load newsfeed in the background + cx.spawn_in(window, async move |this, cx| { + let task: Task> = cx.background_spawn(async move { + let client = client(); + + let mut filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(20); + + if let Some(author) = public_key { + filter = filter.author(author); + }; + + let events = match async_url { + Some(url) => { + client + .fetch_events_from(vec![url], filter, Duration::from_secs(5)) + .await? + } + None => client.fetch_events(filter, Duration::from_secs(5)).await?, + }; + + Ok(events) + }); + + if let Ok(events) = task.await { + this.update(cx, |this, cx| { + this.notes.update(cx, |this, cx| { + *this = Some(events); + cx.notify(); + }); + }) + .ok(); + } + + Ok(()) + }), + ); + + Self { + focus_handle: cx.focus_handle(), + notes, + public_key, + relay_url, + _tasks: tasks, + } + } +} + +impl Panel for Feed { + fn panel_name(&self) -> &'static str { + "Feed" + } + + fn title(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .when_some(self.public_key.as_ref(), |this, public_key| { + let person = PersonRegistry::global(cx); + let profile = person.read(cx).get(public_key, cx); + + this.child( + h_flex() + .gap_1() + .when_some(profile.metadata().picture.as_ref(), |this, url| { + this.child(img(SharedString::from(url)).size_4().rounded_full()) + }) + .child(SharedString::from(profile.name())), + ) + }) + .when_some(self.relay_url.as_ref(), |this, url| { + this.child(SharedString::from(url.to_string())) + }) + } +} + +impl EventEmitter for Feed {} + +impl Focusable for Feed { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for Feed { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let person = PersonRegistry::global(cx); + + v_flex() + .p_2() + .gap_3() + .when_some(self.notes.read(cx).as_ref(), |this, notes| { + this.children({ + let mut items = Vec::with_capacity(notes.len()); + + for note in notes.iter() { + let profile = person.read(cx).get(¬e.pubkey, cx); + + items.push( + v_flex() + .w_full() + .gap_2() + .child( + h_flex() + .w_full() + .items_center() + .justify_between() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child( + h_flex() + .gap_1() + .when_some( + profile.metadata().picture.as_ref(), + |this, url| { + this.child( + img(SharedString::from(url)) + .size_6() + .rounded_full(), + ) + }, + ) + .child(SharedString::from(profile.name())), + ) + .child(SharedString::from( + note.created_at.to_human_datetime(), + )), + ) + .child(SharedString::from(note.content.clone())), + ); + } + + items + }) + }) + } +} diff --git a/crates/lume/src/panels/mod.rs b/crates/lume/src/panels/mod.rs index 564d8c8c..9e5237a6 100644 --- a/crates/lume/src/panels/mod.rs +++ b/crates/lume/src/panels/mod.rs @@ -1 +1,2 @@ +pub mod feed; pub mod startup; diff --git a/crates/lume/src/sidebar/mod.rs b/crates/lume/src/sidebar/mod.rs index 5a1e389b..20eaff20 100644 --- a/crates/lume/src/sidebar/mod.rs +++ b/crates/lume/src/sidebar/mod.rs @@ -11,16 +11,12 @@ use gpui_component::{h_flex, v_flex, ActiveTheme, StyledExt}; use nostr_sdk::prelude::*; use person::PersonRegistry; +use crate::workspace::WorkspaceEvent; + pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Sidebar::new(window, cx)) } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum SidebarEvent { - OpenPublicKey(PublicKey), - OpenRelay(RelayUrl), -} - pub struct Sidebar { focus_handle: FocusHandle, } @@ -40,7 +36,7 @@ impl Panel for Sidebar { } impl EventEmitter for Sidebar {} -impl EventEmitter for Sidebar {} +impl EventEmitter for Sidebar {} impl Focusable for Sidebar { fn focus_handle(&self, _cx: &App) -> FocusHandle { @@ -86,7 +82,7 @@ impl Render for Sidebar { .child(div().text_sm().child(SharedString::from(relay))) .on_click(cx.listener(move |_this, _ev, _window, cx| { if let Ok(url) = RelayUrl::parse(relay) { - cx.emit(SidebarEvent::OpenRelay(url)); + cx.emit(WorkspaceEvent::OpenRelay(url)); } })), ) @@ -123,7 +119,9 @@ impl Render for Sidebar { .hover(|this| this.bg(cx.theme().list_hover)) .child(div().text_sm().child(name.clone())) .on_click(cx.listener(move |_this, _ev, _window, cx| { - cx.emit(SidebarEvent::OpenPublicKey(profile.public_key())); + cx.emit(WorkspaceEvent::OpenPublicKey( + profile.public_key(), + )); })), ) } diff --git a/crates/lume/src/workspace.rs b/crates/lume/src/workspace.rs index 2bd30856..0cd914ea 100644 --- a/crates/lume/src/workspace.rs +++ b/crates/lume/src/workspace.rs @@ -8,17 +8,24 @@ use gpui::{ div, px, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, Window, }; -use gpui_component::dock::{DockArea, DockItem}; +use gpui_component::dock::{DockArea, DockItem, DockPlacement, PanelStyle}; use gpui_component::{v_flex, Root, Theme}; use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; use state::{client, StateEvent}; +use crate::panels::feed::Feed; use crate::panels::startup; -use crate::sidebar::{self, SidebarEvent}; +use crate::sidebar; use crate::title_bar::AppTitleBar; +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum WorkspaceEvent { + OpenPublicKey(PublicKey), + OpenRelay(RelayUrl), +} + #[derive(Debug)] pub struct Workspace { /// The dock area for the workspace. @@ -36,11 +43,20 @@ pub struct Workspace { impl Workspace { pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let dock = cx.new(|cx| DockArea::new("dock", None, window, cx)); - let title_bar = cx.new(|cx| AppTitleBar::new(CLIENT_NAME, window, cx)); let account = Account::global(cx); + // App's title bar + let title_bar = cx.new(|cx| AppTitleBar::new(CLIENT_NAME, window, cx)); + + // Dock area for the workspace. + let dock = + cx.new(|cx| DockArea::new("dock", Some(1), window, cx).panel_style(PanelStyle::TabBar)); + + // Channel for communication between Nostr and GPUI + let (tx, rx) = flume::bounded::(2048); + let mut subscriptions = smallvec![]; + let mut tasks = smallvec![]; // Automatically sync theme with system appearance subscriptions.push(window.observe_window_appearance(|window, cx| { @@ -56,9 +72,6 @@ impl Workspace { }), ); - let mut tasks = smallvec![]; - let (tx, rx) = flume::bounded::(2048); - // Handle nostr notifications tasks.push(cx.background_spawn(async move { let client = client(); @@ -79,9 +92,6 @@ impl Workspace { } match event.kind { - Kind::TextNote => { - // TODO - } Kind::ContactList => { // Get all public keys from the event let public_keys: Vec = @@ -166,13 +176,19 @@ impl Workspace { self._subscriptions.push(cx.subscribe_in( &sidebar, window, - |_this, _sidebar, event: &SidebarEvent, _window, _cx| { + |this, _sidebar, event: &WorkspaceEvent, window, cx| { match event { - SidebarEvent::OpenPublicKey(public_key) => { - log::info!("Open public key: {public_key}"); + WorkspaceEvent::OpenPublicKey(public_key) => { + let view = cx.new(|cx| Feed::new(Some(*public_key), None, window, cx)); + this.dock.update(cx, |this, cx| { + this.add_panel(Arc::new(view), DockPlacement::Center, None, window, cx); + }); } - SidebarEvent::OpenRelay(relay) => { - log::info!("Open relay url: {relay}") + WorkspaceEvent::OpenRelay(relay) => { + let view = cx.new(|cx| Feed::new(None, Some(relay.to_owned()), window, cx)); + this.dock.update(cx, |this, cx| { + this.add_panel(Arc::new(view), DockPlacement::Center, None, window, cx); + }); } }; },