feat: simple poc
This commit is contained in:
4
assets/icons/ellipsis.svg
Normal file
4
assets/icons/ellipsis.svg
Normal 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 |
3
assets/icons/panel-left.svg
Normal file
3
assets/icons/panel-left.svg
Normal 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 |
@@ -1,5 +1,7 @@
|
||||
pub use constants::*;
|
||||
pub use paths::*;
|
||||
pub use utils::*;
|
||||
|
||||
mod constants;
|
||||
mod paths;
|
||||
mod utils;
|
||||
|
||||
11
crates/common/src/utils.rs
Normal file
11
crates/common/src/utils.rs
Normal 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..]
|
||||
)
|
||||
}
|
||||
180
crates/lume/src/panels/feed.rs
Normal file
180
crates/lume/src/panels/feed.rs
Normal 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(¬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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod feed;
|
||||
pub mod startup;
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user