This commit is contained in:
kieran 2025-01-07 14:13:41 +00:00
parent 0e19c1a8f3
commit f7021094bc
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
16 changed files with 155 additions and 100 deletions

9
Cargo.lock generated
View File

@ -1639,7 +1639,7 @@ checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enostr"
version = "0.1.0"
source = "git+https://git.v0l.io/nostr/notedeck.git?rev=e08e30f9125b9cf7391e97a2683ba0034bff1644#e08e30f9125b9cf7391e97a2683ba0034bff1644"
source = "git+https://github.com/damus-io/notedeck?rev=06417ff69e772f24ffd7fb2b025f879463d8c51f#06417ff69e772f24ffd7fb2b025f879463d8c51f"
dependencies = [
"bech32",
"ewebsock",
@ -3023,7 +3023,7 @@ dependencies = [
[[package]]
name = "notedeck"
version = "0.1.0"
source = "git+https://git.v0l.io/nostr/notedeck.git?rev=e08e30f9125b9cf7391e97a2683ba0034bff1644#e08e30f9125b9cf7391e97a2683ba0034bff1644"
source = "git+https://github.com/damus-io/notedeck?rev=06417ff69e772f24ffd7fb2b025f879463d8c51f#06417ff69e772f24ffd7fb2b025f879463d8c51f"
dependencies = [
"base32",
"dirs",
@ -3036,6 +3036,7 @@ dependencies = [
"security-framework",
"serde",
"serde_json",
"sha2",
"strum",
"strum_macros",
"thiserror 2.0.9",
@ -3047,7 +3048,7 @@ dependencies = [
[[package]]
name = "notedeck_chrome"
version = "0.2.0"
source = "git+https://git.v0l.io/nostr/notedeck.git?rev=e08e30f9125b9cf7391e97a2683ba0034bff1644#e08e30f9125b9cf7391e97a2683ba0034bff1644"
source = "git+https://github.com/damus-io/notedeck?rev=06417ff69e772f24ffd7fb2b025f879463d8c51f#06417ff69e772f24ffd7fb2b025f879463d8c51f"
dependencies = [
"android-activity 0.4.3",
"eframe",
@ -3071,7 +3072,7 @@ dependencies = [
[[package]]
name = "notedeck_columns"
version = "0.2.0"
source = "git+https://git.v0l.io/nostr/notedeck.git?rev=e08e30f9125b9cf7391e97a2683ba0034bff1644#e08e30f9125b9cf7391e97a2683ba0034bff1644"
source = "git+https://github.com/damus-io/notedeck?rev=06417ff69e772f24ffd7fb2b025f879463d8c51f#06417ff69e772f24ffd7fb2b025f879463d8c51f"
dependencies = [
"bitflags 2.6.0",
"dirs",

View File

@ -26,9 +26,9 @@ egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "d2ea3b4db21
# notedeck stuff
nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2111948b078b24a1659d0bd5d8570f370269c99b" }
notedeck-chrome = { git = "https://git.v0l.io/nostr/notedeck.git", rev = "e08e30f9125b9cf7391e97a2683ba0034bff1644", package = "notedeck_chrome", optional = true }
notedeck = { git = "https://git.v0l.io/nostr/notedeck.git", rev = "e08e30f9125b9cf7391e97a2683ba0034bff1644", package = "notedeck", optional = true }
enostr = { git = "https://git.v0l.io/nostr/notedeck.git", rev = "e08e30f9125b9cf7391e97a2683ba0034bff1644", package = "enostr", optional = true }
notedeck-chrome = { git = "https://github.com/damus-io/notedeck", rev = "06417ff69e772f24ffd7fb2b025f879463d8c51f", package = "notedeck_chrome", optional = true }
notedeck = { git = "https://github.com/damus-io/notedeck", rev = "06417ff69e772f24ffd7fb2b025f879463d8c51f", package = "notedeck", optional = true }
enostr = { git = "https://github.com/damus-io/notedeck", rev = "06417ff69e772f24ffd7fb2b025f879463d8c51f", package = "enostr", optional = true }
poll-promise = "0.3.0"
ehttp = "0.5.0"

View File

@ -1,14 +1,13 @@
use crate::route::{page, RouteServices, RouteType};
use crate::profiles::ProfileLoader;
use crate::route::{page, RouteAction, RouteServices, RouteType};
use crate::widgets::{Header, NostrWidget};
use eframe::epaint::{FontFamily, Margin};
use eframe::CreationContext;
use egui::{Color32, FontData, FontDefinitions, Ui};
use enostr::ewebsock::{WsEvent, WsMessage};
use egui::{Color32, FontData, FontDefinitions, Theme, Ui, Visuals};
use enostr::{PoolEvent, RelayEvent, RelayMessage};
use log::{error, info, warn};
use nostrdb::Transaction;
use nostrdb::{Filter, Transaction};
use notedeck::AppContext;
use std::ops::Div;
use std::sync::mpsc;
pub struct ZapStreamApp {
@ -17,6 +16,7 @@ pub struct ZapStreamApp {
routes_tx: mpsc::Sender<RouteType>,
widget: Box<dyn NostrWidget>,
profiles: ProfileLoader,
}
impl ZapStreamApp {
@ -34,6 +34,7 @@ impl ZapStreamApp {
Self {
current: RouteType::HomePage,
widget: Box::new(page::HomePage::new()),
profiles: ProfileLoader::new(),
routes_tx: tx,
routes_rx: rx,
}
@ -59,16 +60,26 @@ impl notedeck::App for ZapStreamApp {
}
}
let mut app_frame = egui::containers::Frame::default();
let margin = self.frame_margin();
// reset theme
ui.ctx().set_visuals_of(
Theme::Dark,
Visuals {
panel_fill: Color32::BLACK,
override_text_color: Some(Color32::WHITE),
..Default::default()
},
);
app_frame.inner_margin = margin;
app_frame.stroke.color = Color32::BLACK;
let mut app_frame = egui::containers::Frame::default();
app_frame.inner_margin = self.frame_margin();
// handle app state changes
while let Ok(r) = self.routes_rx.try_recv() {
if let RouteType::Action(a) = r {
match a {
RouteAction::DemandProfile(p) => {
self.profiles.demand(p);
}
_ => info!("Not implemented"),
}
} else {
@ -91,17 +102,16 @@ impl notedeck::App for ZapStreamApp {
egui::CentralPanel::default()
.frame(app_frame)
.show(ui.ctx(), |ui| {
ui.visuals_mut().override_text_color = Some(Color32::WHITE);
let tx = Transaction::new(ctx.ndb).expect("transaction");
// display app
ui.vertical(|ui| {
let mut svc = RouteServices {
router: self.routes_tx.clone(),
tx: Transaction::new(ctx.ndb).expect("transaction"),
tx: &tx,
egui: ui.ctx().clone(),
ctx,
};
Header::new().render(ui, &mut svc);
Header::new().render(ui, &mut svc, &tx);
if let Err(e) = self.widget.update(&mut svc) {
error!("{}", e);
}
@ -109,6 +119,15 @@ impl notedeck::App for ZapStreamApp {
})
.response
});
let profiles = self.profiles.next();
if !profiles.is_empty() {
info!("Profiles: {:?}", profiles);
ctx.pool.subscribe(
"profiles".to_string(),
vec![Filter::new().kinds([0]).authors(&profiles).build()],
);
}
}
}

View File

@ -2,14 +2,15 @@
mod android;
pub mod app;
mod link;
mod note_ref;
mod note_util;
mod note_view;
mod profiles;
mod route;
mod services;
mod stream_info;
mod theme;
mod widgets;
mod note_ref;
#[cfg(target_os = "android")]
use android_activity::AndroidApp;

30
src/profiles.rs Normal file
View File

@ -0,0 +1,30 @@
use std::collections::HashSet;
pub struct ProfileLoader {
queue: HashSet<[u8; 32]>,
fetched: HashSet<[u8; 32]>,
}
impl ProfileLoader {
pub fn new() -> Self {
Self {
queue: HashSet::new(),
fetched: HashSet::new(),
}
}
pub fn demand(&mut self, pubkey: [u8; 32]) {
if self.fetched.contains(&pubkey) {
return;
}
self.queue.insert(pubkey);
}
pub fn next(&mut self) -> Vec<[u8; 32]> {
let ret: Vec<[u8; 32]> = self.queue.drain().collect();
for p in ret.iter() {
self.fetched.insert(*p);
}
ret
}
}

View File

@ -37,7 +37,8 @@ impl NostrWidget for HomePage {
.collect();
let events_live = NotesView::from_vec(
events.iter()
events
.iter()
.filter(|r| matches!(r.status(), StreamStatus::Live))
.collect(),
);
@ -50,7 +51,8 @@ impl NostrWidget for HomePage {
.render(ui, services);
}
let events_planned = NotesView::from_vec(
events.iter()
events
.iter()
.filter(|r| matches!(r.status(), StreamStatus::Planned))
.collect(),
);
@ -63,7 +65,8 @@ impl NostrWidget for HomePage {
.render(ui, services);
}
let events_ended = NotesView::from_vec(
events.iter()
events
.iter()
.filter(|r| matches!(r.status(), StreamStatus::Ended))
.collect(),
);

View File

@ -1,21 +1,18 @@
use crate::link::NostrLink;
use crate::route::home::HomePage;
use crate::route::login::LoginPage;
use crate::route::stream::StreamPage;
use crate::services::ffmpeg_loader::FfmpegLoader;
use crate::widgets::{Header, NostrWidget, PlaceholderRect};
use anyhow::{bail, Result};
use egui::{Context, Image, Response, TextureHandle, Ui};
use egui_inbox::{RequestRepaintTrait, UiInbox, UiInboxSender};
use enostr::{EventClientMessage, Note};
use egui::load::SizedTexture;
use egui::{Context, Image, TextureHandle};
use egui_inbox::RequestRepaintTrait;
use enostr::EventClientMessage;
use itertools::Itertools;
use log::{info, warn};
use nostr::{ClientMessage, Event, EventBuilder, JsonUtil, Kind, Tag};
use nostrdb::{Ndb, NdbProfile, NoteKey, Transaction};
use nostr::{Event, EventBuilder, JsonUtil, Kind, Tag};
use nostrdb::{NdbProfile, NoteKey, Transaction};
use notedeck::{AppContext, ImageCache};
use poll_promise::Promise;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::mpsc;
use std::task::Poll;
mod home;
mod login;
@ -46,12 +43,14 @@ pub enum RouteType {
}
#[derive(PartialEq)]
pub enum RouteAction {}
pub enum RouteAction {
DemandProfile([u8; 32]),
}
pub struct RouteServices<'a, 'ctx> {
pub router: mpsc::Sender<RouteType>,
pub tx: Transaction,
pub egui: Context,
pub tx: &'a Transaction,
pub ctx: &'a mut AppContext<'ctx>,
}
@ -82,8 +81,17 @@ impl<'a, 'ctx> RouteServices<'a, 'ctx> {
/// Load/Fetch profiles
pub fn profile(&self, pk: &[u8; 32]) -> Option<NdbProfile<'a>> {
// TODO
None
let p = self
.ctx
.ndb
.get_profile_by_pubkey(self.tx, pk)
.map(|p| p.record().profile())
.ok()
.flatten();
if p.is_none() {
self.action(RouteAction::DemandProfile(pk.clone()));
}
p
}
/// Load image from URL
@ -116,14 +124,18 @@ impl<'a, 'ctx> RouteServices<'a, 'ctx> {
None
}
}
const BLACK_PIXEL: [u8; 4] = [0, 0, 0, 0];
pub fn image_from_cache<'a>(img_cache: &mut ImageCache, ctx: &Context, url: &str) -> Image<'a> {
let m_cached_promise = img_cache.map().get(url);
if m_cached_promise.is_none() {
if let Some(promise) = img_cache.map().get(url) {
match promise.poll() {
Poll::Ready(Ok(t)) => Image::new(SizedTexture::from_handle(t)),
_ => Image::from_bytes(url.to_string(), &BLACK_PIXEL),
}
} else {
let fetch = fetch_img(img_cache, ctx, url);
img_cache.map_mut().insert(url.to_string(), fetch);
Image::from_bytes(url.to_string(), &BLACK_PIXEL)
}
Image::new(url.to_string())
}
fn fetch_img(
@ -137,7 +149,8 @@ fn fetch_img(
let ctx = ctx.clone();
let url = url.to_owned();
let dst_path = dst_path.clone();
Promise::spawn_async(async move {
Promise::spawn_blocking(move || {
info!("Loading image from disk: {}", dst_path.display());
match FfmpegLoader::new().load_image(dst_path) {
Ok(img) => Ok(ctx.load_texture(&url, img, Default::default())),
Err(e) => Err(notedeck::Error::Generic(e.to_string())),
@ -159,12 +172,18 @@ fn fetch_img_from_net(
let cloned_url = url.to_owned();
let cache_path = cache_path.to_owned();
ehttp::fetch(request, move |response| {
let handle = response.map_err(notedeck::Error::Generic).map(|img| {
std::fs::write(&cache_path, &img.bytes).unwrap();
let img_loaded = FfmpegLoader::new().load_image(cache_path).unwrap();
let handle = response
.and_then(|img| {
std::fs::create_dir_all(cache_path.parent().unwrap()).unwrap();
std::fs::write(&cache_path, &img.bytes).unwrap();
info!("Loading image from net: {}", cloned_url);
let img_loaded = FfmpegLoader::new()
.load_image(cache_path)
.map_err(|e| e.to_string())?;
ctx.load_texture(&cloned_url, img_loaded, Default::default())
});
Ok(ctx.load_texture(&cloned_url, img_loaded, Default::default()))
})
.map_err(notedeck::Error::Generic);
sender.send(handle);
ctx.request_repaint();

View File

@ -6,7 +6,7 @@ use crate::widgets::{
sub_or_poll, Chat, NostrWidget, PlaceholderRect, StreamPlayer, StreamTitle, WriteChat,
};
use egui::{vec2, Align, Frame, Layout, Response, Stroke, Ui, Vec2, Widget};
use nostrdb::{Filter, Note, NoteKey, Subscription};
use nostrdb::{Filter, Note, Subscription};
use crate::note_ref::NoteRef;
use std::borrow::Borrow;
@ -14,7 +14,6 @@ use std::collections::HashSet;
pub struct StreamPage {
link: NostrLink,
event: Option<NoteKey>,
player: Option<StreamPlayer>,
chat: Option<Chat>,
new_msg: WriteChat,
@ -28,7 +27,6 @@ impl StreamPage {
Self {
new_msg: WriteChat::new(link.clone()),
link,
event: None,
chat: None,
player: None,
events: HashSet::new(),
@ -146,7 +144,11 @@ impl StreamPage {
impl NostrWidget for StreamPage {
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response {
let events: Vec<Note> = vec![];
let events: Vec<Note> = self
.events
.iter()
.map_while(|e| services.ctx.ndb.get_note_by_key(services.tx, e.key).ok())
.collect();
if let Some(event) = events.first() {
if let Some(stream) = event.stream() {
@ -173,14 +175,14 @@ impl NostrWidget for StreamPage {
}
fn update(&mut self, services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> {
let filt = self.get_filters();
let filters = self.get_filters();
sub_or_poll(
services.ctx.ndb,
&services.tx,
&mut services.ctx.pool,
&mut self.events,
&mut self.sub,
filt,
filters,
)?;
if let Some(c) = self.chat.as_mut() {
c.update(services)?;

View File

@ -17,11 +17,7 @@ impl FfmpegLoader {
Self::load_image_from_demuxer(demux)
}
pub fn load_image_bytes(
&self,
key: &str,
data: &'static [u8],
) -> Result<ColorImage, Error> {
pub fn load_image_bytes(&self, key: &str, data: &'static [u8]) -> Result<ColorImage, Error> {
let demux = Demuxer::new_custom_io(data, Some(key.to_string()))?;
Self::load_image_from_demuxer(demux)
}
@ -57,6 +53,7 @@ impl FfmpegLoader {
av_frame_free(&mut frame);
let image = video_frame_to_image(frame_rgb);
av_packet_free(&mut pkt);
return Ok(image);
}
}

View File

@ -38,7 +38,7 @@ impl NostrWidget for Chat {
let stream = services
.ctx
.ndb
.get_note_by_key(&services.tx, self.stream)
.get_note_by_key(services.tx, self.stream)
.unwrap();
ScrollArea::vertical()
@ -55,9 +55,10 @@ impl NostrWidget for Chat {
.sorted_by(|a, b| a.created_at.cmp(&b.created_at))
{
if let Ok(ev) =
services.ctx.ndb.get_note_by_key(&services.tx, ev.key)
services.ctx.ndb.get_note_by_key(services.tx, ev.key)
{
ChatMessage::new(&stream, &ev, &None)
let profile = services.profile(ev.pubkey());
ChatMessage::new(&stream, &ev, &profile)
.render(ui, services.ctx.img_cache);
}
}
@ -72,7 +73,7 @@ impl NostrWidget for Chat {
let filters = vec![self.get_filter()];
sub_or_poll(
services.ctx.ndb,
&services.tx,
services.tx,
&mut services.ctx.pool,
&mut self.events,
&mut self.sub,

View File

@ -33,7 +33,9 @@ impl<'a> ChatMessage<'a> {
job.wrap.break_anywhere = true;
let is_host = self.stream.host().eq(self.ev.pubkey());
let name = self.profile.map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f));
let name = self
.profile
.map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f));
let name_color = if is_host { PRIMARY } else { NEUTRAL_500 };

View File

@ -4,6 +4,7 @@ use crate::widgets::{Button, NostrWidget};
use eframe::emath::Align;
use eframe::epaint::Vec2;
use egui::{CursorIcon, Frame, Layout, Margin, Response, Sense, Ui, Widget};
use nostrdb::Transaction;
pub struct Header;
@ -11,10 +12,12 @@ impl Header {
pub fn new() -> Self {
Self {}
}
}
impl NostrWidget for Header {
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response {
pub fn render(
&mut self,
ui: &mut Ui,
services: &mut RouteServices<'_, '_>,
tx: &Transaction,
) -> Response {
let logo_bytes = include_bytes!("../resources/logo.svg");
Frame::none()
.outer_margin(Margin::symmetric(16., 8.))
@ -37,7 +40,8 @@ impl NostrWidget for Header {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if let Some(acc) = services.ctx.accounts.get_selected_account() {
Avatar::pubkey(&acc.pubkey, services.ctx.ndb, &services.tx).render(ui, services.ctx.img_cache);
Avatar::pubkey(&acc.pubkey, services.ctx.ndb, tx)
.render(ui, services.ctx.img_cache);
} else if Button::new().show(ui, |ui| ui.label("Login")).clicked() {
services.navigate(RouteType::LoginPage);
}
@ -47,8 +51,4 @@ impl NostrWidget for Header {
})
.response
}
fn update(&mut self, _services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> {
Ok(())
}
}

View File

@ -66,4 +66,4 @@ impl<'a> StreamList<'a> {
})
.response
}
}
}

View File

@ -21,9 +21,8 @@ impl<'a> StreamEvent<'a> {
pub fn new(event: &'a Note<'a>) -> Self {
Self { event }
}
}
impl NostrWidget for StreamEvent<'_> {
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response {
pub fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response {
ui.vertical(|ui| {
ui.style_mut().spacing.item_spacing = Vec2::new(12., 16.);
@ -127,8 +126,4 @@ impl NostrWidget for StreamEvent<'_> {
})
.response
}
fn update(&mut self, _services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> {
Ok(())
}
}

View File

@ -13,10 +13,7 @@ impl<'a> StreamTitle<'a> {
pub fn new(event: &'a Note<'a>) -> StreamTitle<'a> {
StreamTitle { event }
}
}
impl NostrWidget for StreamTitle<'_> {
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response {
pub fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response {
Frame::none()
.outer_margin(Margin::symmetric(12., 8.))
.show(ui, |ui| {
@ -43,8 +40,4 @@ impl NostrWidget for StreamTitle<'_> {
})
.response
}
fn update(&mut self, _services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> {
Ok(())
}
}

View File

@ -1,12 +1,10 @@
use crate::link::NostrLink;
use crate::route::RouteServices;
use crate::theme::{MARGIN_DEFAULT, NEUTRAL_900, ROUNDING_DEFAULT};
use crate::widgets::{NativeTextInput, NostrWidget};
use crate::widgets::NativeTextInput;
use eframe::emath::Align;
use egui::{Frame, Layout, Response, Sense, Ui, Widget};
use log::info;
use nostrdb::Filter;
use notedeck::AppContext;
pub struct WriteChat {
link: NostrLink,
@ -20,10 +18,8 @@ impl WriteChat {
msg: String::new(),
}
}
}
impl NostrWidget for WriteChat {
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response {
pub fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response {
let logo_bytes = include_bytes!("../resources/send-03.svg");
Frame::none()
.inner_margin(MARGIN_DEFAULT)
@ -52,8 +48,4 @@ impl NostrWidget for WriteChat {
})
.response
}
fn update(&mut self, _services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> {
Ok(())
}
}