feat: simple poc

This commit is contained in:
2025-12-16 09:18:23 +07:00
parent 15cefbd84f
commit 0be73e8e82
8 changed files with 239 additions and 24 deletions

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm8.25 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-16.5 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"/>
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm8.25 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-16.5 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 457 B

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="1.5" d="M3.75 5.75a2 2 0 0 1 2-2h12.5a2 2 0 0 1 2 2v12.5a2 2 0 0 1-2 2H5.75a2 2 0 0 1-2-2V5.75Zm5-1.75v16"/>
</svg>

After

Width:  |  Height:  |  Size: 303 B

View File

@@ -1,5 +1,7 @@
pub use constants::*;
pub use paths::*;
pub use utils::*;
mod constants;
mod paths;
mod utils;

View File

@@ -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..]
)
}

View File

@@ -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<Option<Events>>,
/// Public Key
public_key: Option<PublicKey>,
/// Relay Url
relay_url: Option<RelayUrl>,
/// Async operations
_tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
}
impl Feed {
pub fn new(
public_key: Option<PublicKey>,
relay_url: Option<RelayUrl>,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Result<Events, Error>> = 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<Self>) -> 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<PanelEvent> 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<Self>) -> 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(&note.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
})
})
}
}

View File

@@ -1 +1,2 @@
pub mod feed;
pub mod startup;

View File

@@ -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<Sidebar> {
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<PanelEvent> for Sidebar {}
impl EventEmitter<SidebarEvent> for Sidebar {}
impl EventEmitter<WorkspaceEvent> 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(),
));
})),
)
}

View File

@@ -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>) -> 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::<StateEvent>(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::<StateEvent>(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<PublicKey> =
@@ -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);
});
}
};
},