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 constants::*;
|
||||||
pub use paths::*;
|
pub use paths::*;
|
||||||
|
pub use utils::*;
|
||||||
|
|
||||||
mod constants;
|
mod constants;
|
||||||
mod paths;
|
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;
|
pub mod startup;
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ use gpui_component::{h_flex, v_flex, ActiveTheme, StyledExt};
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
|
|
||||||
|
use crate::workspace::WorkspaceEvent;
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
||||||
cx.new(|cx| Sidebar::new(window, cx))
|
cx.new(|cx| Sidebar::new(window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum SidebarEvent {
|
|
||||||
OpenPublicKey(PublicKey),
|
|
||||||
OpenRelay(RelayUrl),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
}
|
}
|
||||||
@@ -40,7 +36,7 @@ impl Panel for Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for Sidebar {}
|
impl EventEmitter<PanelEvent> for Sidebar {}
|
||||||
impl EventEmitter<SidebarEvent> for Sidebar {}
|
impl EventEmitter<WorkspaceEvent> for Sidebar {}
|
||||||
|
|
||||||
impl Focusable for Sidebar {
|
impl Focusable for Sidebar {
|
||||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
@@ -86,7 +82,7 @@ impl Render for Sidebar {
|
|||||||
.child(div().text_sm().child(SharedString::from(relay)))
|
.child(div().text_sm().child(SharedString::from(relay)))
|
||||||
.on_click(cx.listener(move |_this, _ev, _window, cx| {
|
.on_click(cx.listener(move |_this, _ev, _window, cx| {
|
||||||
if let Ok(url) = RelayUrl::parse(relay) {
|
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))
|
.hover(|this| this.bg(cx.theme().list_hover))
|
||||||
.child(div().text_sm().child(name.clone()))
|
.child(div().text_sm().child(name.clone()))
|
||||||
.on_click(cx.listener(move |_this, _ev, _window, cx| {
|
.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,
|
div, px, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||||
Render, Styled, Subscription, Task, Window,
|
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 gpui_component::{v_flex, Root, Theme};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::{client, StateEvent};
|
use state::{client, StateEvent};
|
||||||
|
|
||||||
|
use crate::panels::feed::Feed;
|
||||||
use crate::panels::startup;
|
use crate::panels::startup;
|
||||||
use crate::sidebar::{self, SidebarEvent};
|
use crate::sidebar;
|
||||||
use crate::title_bar::AppTitleBar;
|
use crate::title_bar::AppTitleBar;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum WorkspaceEvent {
|
||||||
|
OpenPublicKey(PublicKey),
|
||||||
|
OpenRelay(RelayUrl),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
/// The dock area for the workspace.
|
/// The dock area for the workspace.
|
||||||
@@ -36,11 +43,20 @@ pub struct Workspace {
|
|||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
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);
|
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 subscriptions = smallvec![];
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
// Automatically sync theme with system appearance
|
// Automatically sync theme with system appearance
|
||||||
subscriptions.push(window.observe_window_appearance(|window, cx| {
|
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
|
// Handle nostr notifications
|
||||||
tasks.push(cx.background_spawn(async move {
|
tasks.push(cx.background_spawn(async move {
|
||||||
let client = client();
|
let client = client();
|
||||||
@@ -79,9 +92,6 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match event.kind {
|
match event.kind {
|
||||||
Kind::TextNote => {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
Kind::ContactList => {
|
Kind::ContactList => {
|
||||||
// Get all public keys from the event
|
// Get all public keys from the event
|
||||||
let public_keys: Vec<PublicKey> =
|
let public_keys: Vec<PublicKey> =
|
||||||
@@ -166,13 +176,19 @@ impl Workspace {
|
|||||||
self._subscriptions.push(cx.subscribe_in(
|
self._subscriptions.push(cx.subscribe_in(
|
||||||
&sidebar,
|
&sidebar,
|
||||||
window,
|
window,
|
||||||
|_this, _sidebar, event: &SidebarEvent, _window, _cx| {
|
|this, _sidebar, event: &WorkspaceEvent, window, cx| {
|
||||||
match event {
|
match event {
|
||||||
SidebarEvent::OpenPublicKey(public_key) => {
|
WorkspaceEvent::OpenPublicKey(public_key) => {
|
||||||
log::info!("Open public key: {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) => {
|
WorkspaceEvent::OpenRelay(relay) => {
|
||||||
log::info!("Open relay url: {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