diff --git a/Cargo.lock b/Cargo.lock index eb22c22..fe35670 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1032,6 +1032,7 @@ dependencies = [ [[package]] name = "egui-video" version = "0.8.0" +source = "git+https://github.com/v0l/egui-video.git?rev=4766d939ce4d34b5a3a57b2fbe750ea10f389f39#4766d939ce4d34b5a3a57b2fbe750ea10f389f39" dependencies = [ "anyhow", "atomic", diff --git a/Cargo.toml b/Cargo.toml index 15f8ff4..9e92344 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,4 @@ pretty_env_logger = "0.5.0" egui_inbox = "0.6.0" bech32 = "0.11.0" libc = "0.2.158" -egui-video = { path = "../egui-video" } +egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "4766d939ce4d34b5a3a57b2fbe750ea10f389f39" } diff --git a/src/app.rs b/src/app.rs index 84ce454..1cbc00e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,15 +1,12 @@ use crate::route::Router; use eframe::{App, CreationContext, Frame}; -use egui::{Color32, Context, ScrollArea}; -use egui_inbox::UiInbox; -use log::warn; -use nostr_sdk::database::{MemoryDatabase, MemoryDatabaseOptions}; -use nostr_sdk::{Client, Filter, JsonUtil, Kind, RelayPoolNotification}; +use egui::{Color32, Context}; +use nostr_sdk::database::MemoryDatabase; +use nostr_sdk::{Client, RelayPoolNotification}; use nostrdb::{Config, Ndb}; use tokio::sync::broadcast; pub struct ZapStreamApp { - ndb: Ndb, client: Client, notifications: broadcast::Receiver, router: Router, @@ -17,10 +14,7 @@ pub struct ZapStreamApp { impl ZapStreamApp { pub fn new(cc: &CreationContext) -> Self { - let client = Client::builder().database(MemoryDatabase::with_opts(MemoryDatabaseOptions { - events: true, - ..Default::default() - })).build(); + let client = Client::builder().database(MemoryDatabase::with_opts(Default::default())).build(); let notifications = client.notifications(); let ctx_clone = cc.egui_ctx.clone(); @@ -28,10 +22,6 @@ impl ZapStreamApp { tokio::spawn(async move { client_clone.add_relay("wss://nos.lol").await.expect("Failed to add relay"); client_clone.connect().await; - client_clone.subscribe(vec![ - Filter::new() - .kind(Kind::LiveEvent) - ], None).await.expect("Failed to subscribe"); let mut notifications = client_clone.notifications(); while let Ok(_) = notifications.recv().await { ctx_clone.request_repaint(); @@ -39,36 +29,18 @@ impl ZapStreamApp { }); egui_extras::install_image_loaders(&cc.egui_ctx); - let inbox = UiInbox::new(); let ndb = Ndb::new(".", &Config::default()).unwrap(); + Self { - ndb: ndb.clone(), client: client.clone(), notifications, - router: Router::new(inbox, cc.egui_ctx.clone(), client.clone(), ndb.clone()), - } - } - - fn process_nostr(&mut self) { - while let Ok(msg) = self.notifications.try_recv() { - match msg { - RelayPoolNotification::Event { event, .. } => { - if let Err(e) = self.ndb.process_event(event.as_json().as_str()) { - warn!("Failed to process event: {:?}", e); - } - } - _ => { - // dont care - } - } + router: Router::new(cc.egui_ctx.clone(), client.clone(), ndb.clone()), } } } impl App for ZapStreamApp { fn update(&mut self, ctx: &Context, frame: &mut Frame) { - self.process_nostr(); - let mut app_frame = egui::containers::Frame::default(); app_frame.stroke.color = Color32::BLACK; diff --git a/src/link.rs b/src/link.rs index 2b64618..9ca0d54 100644 --- a/src/link.rs +++ b/src/link.rs @@ -5,22 +5,31 @@ use nostr_sdk::util::hex; use nostrdb::{Filter, Note}; use std::fmt::{Display, Formatter}; -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub struct NostrLink { - hrp: NostrLinkType, - id: IdOrStr, - kind: Option, - author: Option<[u8; 32]>, - relays: Vec, + pub hrp: NostrLinkType, + pub id: IdOrStr, + pub kind: Option, + pub author: Option<[u8; 32]>, + pub relays: Vec, } -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub enum IdOrStr { Id([u8; 32]), Str(String), } -#[derive(Clone)] +impl Display for IdOrStr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + IdOrStr::Id(id) => write!(f, "{}", hex::encode(id)), + IdOrStr::Str(str) => write!(f, "{}", str), + } + } +} + +#[derive(Clone, Eq, PartialEq)] pub enum NostrLinkType { Note, PublicKey, @@ -62,6 +71,22 @@ impl NostrLink { } } } + + pub fn to_tag(&self) -> Vec { + if self.hrp == NostrLinkType::Coordinate { + vec!["a".to_string(), self.to_tag_value()] + } else { + vec!["e".to_string(), self.to_tag_value()] + } + } + + pub fn to_tag_value(&self) -> String { + if self.hrp == NostrLinkType::Coordinate { + format!("{}:{}:{}", self.kind.unwrap(), hex::encode(self.author.unwrap()), self.id) + } else { + self.id.to_string() + } + } } impl TryInto for &NostrLink { diff --git a/src/note_util.rs b/src/note_util.rs index 55eee32..3903a43 100644 --- a/src/note_util.rs +++ b/src/note_util.rs @@ -1,9 +1,6 @@ -use libc::{malloc, memcpy}; use nostr_sdk::util::hex; -use nostrdb::{NdbStr, Note, Tag, Tags}; +use nostrdb::{NdbStr, Note, Tag}; use std::fmt::Display; -use std::mem::transmute; -use std::{mem, ptr}; pub trait NoteUtil { fn id_hex(&self) -> String; @@ -70,4 +67,5 @@ impl<'a> Iterator for TagIterBorrow<'a> { } } -pub struct OwnedNote; \ No newline at end of file +#[derive(Eq, PartialEq)] +pub struct OwnedNote(pub u64); \ No newline at end of file diff --git a/src/route/home.rs b/src/route/home.rs index de1b4fd..ef6e256 100644 --- a/src/route/home.rs +++ b/src/route/home.rs @@ -1,27 +1,52 @@ +use crate::note_util::OwnedNote; use crate::route::RouteServices; -use egui::{Response, Ui, Widget}; -use nostrdb::{Filter, Note}; +use crate::services::ndb_wrapper::NDBWrapper; use crate::widgets; +use crate::widgets::NostrWidget; +use egui::{Response, Ui, Widget}; +use log::{error, info}; +use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction}; -pub struct HomePage<'a> { - services: &'a RouteServices<'a>, +pub struct HomePage { + sub: Subscription, + events: Vec, + ndb: NDBWrapper, } -impl<'a> HomePage<'a> { - pub fn new(services: &'a RouteServices) -> Self { - Self { services } - } -} - -impl<'a> Widget for HomePage<'a> { - fn ui(self, ui: &mut Ui) -> Response { - let events = self.services.ndb.query(&self.services.tx, &[ +impl HomePage { + pub fn new(ndb: &NDBWrapper, tx: &Transaction) -> Self { + let filter = [ Filter::new() .kinds([30_311]) .limit(10) .build() - ], 10).unwrap(); - let events: Vec> = events.iter().map(|v| v.note.clone()).collect(); - widgets::StreamList::new(&events, &self.services).ui(ui) + ]; + let (sub, events) = ndb.subscribe_with_results(&filter, tx, 100); + Self { + sub, + events: events.iter().map(|e| OwnedNote(e.note_key.as_u64())).collect(), + ndb: ndb.clone(), + } + } +} + +impl Drop for HomePage { + fn drop(&mut self) { + self.ndb.unsubscribe(self.sub); + } +} + +impl NostrWidget for HomePage { + fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response { + let new_notes = services.ndb.poll(self.sub, 100); + new_notes.iter().for_each(|n| self.events.push(OwnedNote(n.as_u64()))); + + let events: Vec> = self.events.iter() + .map(|n| services.ndb.get_note_by_key(services.tx, NoteKey::new(n.0))) + .map_while(|f| f.map_or(None, |f| Some(f))) + .collect(); + + info!("HomePage events: {}", events.len()); + widgets::StreamList::new(&events, &services).ui(ui) } } \ No newline at end of file diff --git a/src/route/mod.rs b/src/route/mod.rs index efbee36..0aaf068 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -3,18 +3,22 @@ use crate::note_util::OwnedNote; use crate::route; use crate::route::home::HomePage; use crate::route::stream::StreamPage; +use crate::services::ndb_wrapper::NDBWrapper; use crate::services::profile::ProfileService; -use crate::widgets::{Header, StreamList}; +use crate::widgets::{Header, NostrWidget, StreamList}; use egui::{Context, Response, ScrollArea, Ui, Widget}; use egui_inbox::{UiInbox, UiInboxSender}; -use egui_video::Player; +use egui_video::{Player, PlayerState}; use log::{info, warn}; -use nostr_sdk::Client; +use nostr_sdk::nips::nip01; +use nostr_sdk::{Client, Kind, PublicKey}; use nostrdb::{Filter, Ndb, Note, Transaction}; +use std::borrow::Borrow; mod stream; mod home; +#[derive(PartialEq)] pub enum Routes { HomePage, Event { @@ -30,85 +34,90 @@ pub enum Routes { Action(RouteAction), } +#[derive(PartialEq)] pub enum RouteAction { Login([u8; 32]), - StartPlayer(String), - PausePlayer, - SeekPlayer(f32), - StopPlayer, } pub struct Router { current: Routes, + current_widget: Option>, router: UiInbox, ctx: Context, profile_service: ProfileService, - ndb: Ndb, + ndb: NDBWrapper, login: Option<[u8; 32]>, - player: Option, + client: Client, } impl Router { - pub fn new(rx: UiInbox, ctx: Context, client: Client, ndb: Ndb) -> Self { + pub fn new(ctx: Context, client: Client, ndb: Ndb) -> Self { Self { current: Routes::HomePage, - router: rx, + current_widget: None, + router: UiInbox::new(), ctx: ctx.clone(), profile_service: ProfileService::new(client.clone(), ctx.clone()), - ndb, + ndb: NDBWrapper::new(ctx.clone(), ndb.clone(), client.clone()), + client, login: None, - player: None, } } + fn load_widget(&mut self, route: Routes, tx: &Transaction) { + match &route { + Routes::HomePage => { + let w = HomePage::new(&self.ndb, tx); + self.current_widget = Some(Box::new(w)); + } + Routes::Event { link, .. } => { + let w = StreamPage::new_from_link(&self.ndb, tx, link.clone()); + self.current_widget = Some(Box::new(w)); + } + _ => warn!("Not implemented") + } + self.current = route; + } + pub fn show(&mut self, ui: &mut Ui) -> Response { - let tx = Transaction::new(&self.ndb).unwrap(); + let tx = self.ndb.start_transaction(); + // handle app state changes while let Some(r) = self.router.read(ui).next() { - if let Routes::Action(a) = r { + if let Routes::Action(a) = &r { match a { RouteAction::Login(k) => { - self.login = Some(k) - } - RouteAction::StartPlayer(u) => { - if self.player.is_none() { - if let Ok(p) = Player::new(&self.ctx, &u) { - self.player = Some(p) - } - } + self.login = Some(k.clone()) } _ => info!("Not implemented") } } else { - self.current = r; + self.load_widget(r, &tx); } } - let mut svc = RouteServices { + // load homepage on start + if self.current_widget.is_none() { + self.load_widget(Routes::HomePage, &tx); + } + + let svc = RouteServices { context: self.ctx.clone(), profile: &self.profile_service, router: self.router.sender(), ndb: self.ndb.clone(), tx: &tx, login: &self.login, - player: &mut self.player, }; // display app ScrollArea::vertical().show(ui, |ui| { ui.add(Header::new(&svc)); - match &self.current { - Routes::HomePage => { - HomePage::new(&svc).ui(ui) - } - Routes::Event { link, event } => { - StreamPage::new(&mut svc, link, event) - .ui(ui) - } - _ => { - ui.label("Not found") - } + if let Some(w) = self.current_widget.as_mut() { + w.render(ui, &svc) + } else { + ui.label("No widget") } }).inner } @@ -117,9 +126,8 @@ impl Router { pub struct RouteServices<'a> { pub context: Context, //cloned pub router: UiInboxSender, //cloned - pub ndb: Ndb, //cloned + pub ndb: NDBWrapper, //cloned - pub player: &'a mut Option, pub profile: &'a ProfileService, //ref pub tx: &'a Transaction, //ref pub login: &'a Option<[u8; 32]>, //ref diff --git a/src/route/stream.rs b/src/route/stream.rs index 0fc5160..2b2fdac 100644 --- a/src/route/stream.rs +++ b/src/route/stream.rs @@ -1,51 +1,83 @@ use crate::link::NostrLink; use crate::note_util::{NoteUtil, OwnedNote}; -use crate::route::{RouteAction, RouteServices, Routes}; +use crate::route::RouteServices; +use crate::services::ndb_wrapper::NDBWrapper; use crate::stream_info::StreamInfo; -use crate::widgets::StreamPlayer; +use crate::widgets::{Chat, NostrWidget, StreamPlayer}; use egui::{Color32, Label, Response, RichText, TextWrapMode, Ui, Widget}; -use nostrdb::Note; -use std::ptr; +use nostrdb::{Filter, NoteKey, Subscription, Transaction}; +use std::borrow::Borrow; -pub struct StreamPage<'a> { - services: &'a mut RouteServices<'a>, - link: &'a NostrLink, - event: &'a Option, +pub struct StreamPage { + link: NostrLink, + event: Option, + player: Option, + chat: Option, + sub: Subscription, } -impl<'a> StreamPage<'a> { - pub fn new(services: &'a mut RouteServices<'a>, link: &'a NostrLink, event: &'a Option) -> Self { - Self { services, link, event } +impl StreamPage { + pub fn new_from_link(ndb: &NDBWrapper, tx: &Transaction, link: NostrLink) -> Self { + let f: Filter = link.borrow().try_into().unwrap(); + let f = [ + f.limit_mut(1) + ]; + let (sub, events) = ndb.subscribe_with_results(&f, tx, 1); + Self { + link, + sub, + event: events.first().map_or(None, |n| Some(OwnedNote(n.note_key.as_u64()))), + chat: None, + player: None, + } } } -impl<'a> Widget for StreamPage<'a> { - fn ui(self, ui: &mut Ui) -> Response { - let event = if let Some(event) = self.event { - Note::Owned { - ptr: ptr::null_mut(), - size: 0, +impl NostrWidget for StreamPage { + fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response { + let poll = services.ndb.poll(self.sub, 1); + if let Some(k) = poll.first() { + self.event = Some(OwnedNote(k.as_u64())) + } + + let event = if let Some(k) = &self.event { + services.ndb.get_note_by_key(services.tx, NoteKey::new(k.0)) + .map_or(None, |f| Some(f)) + } else { + None + }; + if let Some(event) = event { + if let Some(stream) = event.stream() { + if self.player.is_none() { + let p = StreamPlayer::new(ui.ctx(), &stream); + self.player = Some(p); + } + } + + if let Some(player) = &mut self.player { + player.ui(ui); + } + + let title = RichText::new(match event.get_tag_value("title") { + Some(s) => s.variant().str().unwrap_or("Unknown"), + None => "Unknown", + }) + .size(16.) + .color(Color32::WHITE); + ui.add(Label::new(title).wrap_mode(TextWrapMode::Truncate)); + + if self.chat.is_none() { + let chat = Chat::new(self.link.clone(), &services.ndb, services.tx); + self.chat = Some(chat); + } + + if let Some(c) = self.chat.as_mut() { + c.render(ui, services) + } else { + ui.label("Loading..") } } else { - let mut q = self.services.ndb.query(self.services.tx, &[ - self.link.try_into().unwrap() - ], 1).unwrap(); - let [e] = q.try_into().unwrap(); - e.note - }; - - if let Some(stream) = event.stream() { - if self.services.player.is_none() { - self.services.navigate(Routes::Action(RouteAction::StartPlayer(stream))); - } + ui.label("Loading..") } - StreamPlayer::new(self.services).ui(ui); - let title = RichText::new(match event.get_tag_value("title") { - Some(s) => s.variant().str().unwrap_or("Unknown"), - None => "Unknown", - }) - .size(16.) - .color(Color32::WHITE); - ui.add(Label::new(title).wrap_mode(TextWrapMode::Truncate)) } } \ No newline at end of file diff --git a/src/services/mod.rs b/src/services/mod.rs index 5b3bafd..7b874d9 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1 +1,2 @@ -pub mod profile; \ No newline at end of file +pub mod profile; +pub mod ndb_wrapper; \ No newline at end of file diff --git a/src/services/ndb_wrapper.rs b/src/services/ndb_wrapper.rs new file mode 100644 index 0000000..f23c3bc --- /dev/null +++ b/src/services/ndb_wrapper.rs @@ -0,0 +1,82 @@ +use egui::CursorIcon::Default; +use log::{info, warn}; +use nostr_sdk::secp256k1::Context; +use nostr_sdk::{Client, JsonUtil, Kind, RelayPoolNotification}; +use nostrdb::{Error, Filter, Ndb, Note, NoteKey, ProfileRecord, QueryResult, Subscription, Transaction}; +use tokio::sync::mpsc::UnboundedSender; + +#[derive(Debug, Clone)] +pub struct NDBWrapper { + ctx: egui::Context, + ndb: Ndb, + client: Client, +} + +impl NDBWrapper { + pub fn new(ctx: egui::Context, ndb: Ndb, client: Client) -> Self { + let client_clone = client.clone(); + let ndb_clone = ndb.clone(); + let ctx_clone = ctx.clone(); + tokio::spawn(async move { + let mut notifications = client_clone.notifications(); + while let Ok(e) = notifications.recv().await { + match e { + RelayPoolNotification::Event { event, .. } => { + if let Err(e) = ndb_clone.process_event(event.as_json().as_str()) { + warn!("Failed to process event: {:?}", e); + } else { + ctx_clone.request_repaint(); + } + } + _ => { + // dont care + } + } + } + }); + Self { ctx, ndb, client } + } + + pub fn start_transaction(&self) -> Transaction { + Transaction::new(&self.ndb).unwrap() + } + + pub fn subscribe(&self, filters: &[Filter]) -> Subscription { + let sub = self.ndb.subscribe(filters).unwrap(); + let c_clone = self.client.clone(); + let filters = filters.iter().map(|f| nostr_sdk::Filter::from_json(f.json().unwrap()).unwrap()).collect(); + let id_clone = sub.id(); + tokio::spawn(async move { + let nostr_sub = c_clone.subscribe(filters, None).await.unwrap(); + info!("Sub mapping {}->{}", id_clone, nostr_sub.id()) + }); + sub + } + + pub fn unsubscribe(&self, sub: Subscription) { + self.ndb.unsubscribe(sub).unwrap() + } + + pub fn subscribe_with_results<'a>(&self, filters: &[Filter], tx: &'a Transaction, max_results: i32) -> (Subscription, Vec>) { + let sub = self.subscribe(filters); + let q = self.query(tx, filters, max_results); + (sub, q) + } + + + pub fn query<'a>(&self, tx: &'a Transaction, filters: &[Filter], max_results: i32) -> Vec> { + self.ndb.query(tx, filters, max_results).unwrap() + } + + pub fn poll(&self, sub: Subscription, max_results: u32) -> Vec { + self.ndb.poll_for_notes(sub, max_results) + } + + pub fn get_note_by_key<'a>(&self, tx: &'a Transaction, key: NoteKey) -> Result, Error> { + self.ndb.get_note_by_key(tx, key) + } + + pub fn get_profile_by_pubkey<'a>(&self, tx: &'a Transaction, pubkey: &[u8; 32]) -> Result, Error> { + self.ndb.get_profile_by_pubkey(tx, pubkey) + } +} \ No newline at end of file diff --git a/src/widgets/avatar.rs b/src/widgets/avatar.rs index ddb45bc..c677376 100644 --- a/src/widgets/avatar.rs +++ b/src/widgets/avatar.rs @@ -1,5 +1,5 @@ -use egui::{Color32, Image, Rect, Response, Rounding, Sense, Ui, Vec2, Widget}; use crate::services::profile::ProfileService; +use egui::{Color32, Image, Rect, Response, Rounding, Sense, Ui, Vec2, Widget}; pub struct Avatar<'a> { image: Option>, @@ -10,6 +10,10 @@ impl<'a> Avatar<'a> { Self { image: Some(img) } } + pub fn new_optional(img: Option>) -> Self { + Self { image: img } + } + pub fn public_key(svc: &'a ProfileService, pk: &[u8; 32]) -> Self { if let Some(meta) = svc.get_profile(pk) { if let Some(img) = &meta.picture { @@ -45,8 +49,10 @@ impl<'a> Widget for Avatar<'a> { img.rounding(Rounding::same(ui.available_height())).ui(ui) } None => { - let (response, painter) = ui.allocate_painter(Vec2::new(32., 32.), Sense::hover()); - painter.rect_filled(Rect::EVERYTHING, Rounding::same(32.), Color32::from_rgb(200, 200, 200)); + let h = ui.available_height(); + let rnd = Rounding::same(h); + let (response, painter) = ui.allocate_painter(Vec2::new(h,h), Sense::click()); + painter.rect_filled(Rect::EVERYTHING, rnd, Color32::from_rgb(200, 200, 200)); response } } diff --git a/src/widgets/chat.rs b/src/widgets/chat.rs new file mode 100644 index 0000000..0835521 --- /dev/null +++ b/src/widgets/chat.rs @@ -0,0 +1,54 @@ +use crate::link::NostrLink; +use crate::note_util::OwnedNote; +use crate::route::RouteServices; +use crate::services::ndb_wrapper::NDBWrapper; +use crate::widgets::chat_message::ChatMessage; +use crate::widgets::NostrWidget; +use egui::{Response, ScrollArea, Ui, Widget}; +use nostrdb::{Filter, Note, NoteKey, Subscription, Transaction}; +use std::borrow::Borrow; + +pub struct Chat { + link: NostrLink, + events: Vec, + sub: Subscription, +} + +impl Chat { + pub fn new(link: NostrLink, ndb: &NDBWrapper, tx: &Transaction) -> Self { + let filter = Filter::new() + .kinds([1_311]) + .tags([link.to_tag_value()], 'a') + .build(); + let filter = [filter]; + + let (sub, events) = ndb.subscribe_with_results(&filter, tx, 500); + + Self { + link, + sub, + events: events.iter().map(|n| OwnedNote(n.note_key.as_u64())).collect(), + } + } +} + +impl NostrWidget for Chat { + fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response { + let poll = services.ndb.poll(self.sub, 500); + poll.iter().for_each(|n| self.events.push(OwnedNote(n.as_u64()))); + + let events: Vec = self.events.iter().map_while(|n| + services.ndb + .get_note_by_key(services.tx, NoteKey::new(n.0)) + .map_or(None, |n| Some(n)) + ).collect(); + + ScrollArea::vertical().show(ui, |ui| { + ui.vertical(|ui| { + for ev in events { + ChatMessage::new(&ev, services).ui(ui); + } + }).response + }).inner + } +} \ No newline at end of file diff --git a/src/widgets/chat_message.rs b/src/widgets/chat_message.rs new file mode 100644 index 0000000..2d66c94 --- /dev/null +++ b/src/widgets/chat_message.rs @@ -0,0 +1,31 @@ +use crate::route::RouteServices; +use crate::widgets::Profile; +use eframe::epaint::Vec2; +use egui::{Response, Ui, Widget}; +use nostrdb::Note; + +pub struct ChatMessage<'a> { + ev: &'a Note<'a>, + services: &'a RouteServices<'a>, +} + +impl<'a> ChatMessage<'a> { + pub fn new(ev: &'a Note<'a>, services: &'a RouteServices<'a>) -> ChatMessage<'a> { + ChatMessage { ev, services } + } +} + +impl<'a> Widget for ChatMessage<'a> { + fn ui(self, ui: &mut Ui) -> Response { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing = Vec2::new(8., 2.); + let author = self.ev.pubkey(); + Profile::new(author, self.services) + .size(24.) + .ui(ui); + + let content = self.ev.content(); + ui.label(content); + }).response + } +} \ No newline at end of file diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 30645fd..2eb42f3 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -4,8 +4,21 @@ mod stream_list; mod avatar; mod stream_player; mod video_placeholder; +mod chat; +mod chat_message; +mod profile; +use egui::{Response, Ui}; +use crate::route::RouteServices; + +pub trait NostrWidget { + fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response; +} + +pub use self::avatar::Avatar; pub use self::header::Header; pub use self::stream_list::StreamList; pub use self::video_placeholder::VideoPlaceholder; pub use self::stream_player::StreamPlayer; +pub use self::profile::Profile; +pub use self::chat::Chat; diff --git a/src/widgets/profile.rs b/src/widgets/profile.rs new file mode 100644 index 0000000..133181b --- /dev/null +++ b/src/widgets/profile.rs @@ -0,0 +1,41 @@ +use crate::route::RouteServices; +use crate::widgets::Avatar; +use egui::{Color32, Image, Label, Response, RichText, TextWrapMode, Ui, Widget}; +use nostrdb::NdbProfile; + +pub struct Profile<'a> { + size: f32, + pubkey: &'a [u8; 32], + profile: Option>, +} + +impl<'a> Profile<'a> { + pub fn new(pubkey: &'a [u8; 32], services: &'a RouteServices<'a>) -> Self { + let p = services.ndb.get_profile_by_pubkey(services.tx, &pubkey) + .map(|f| f.record().profile()) + .map_or(None, |r| r); + + Self { pubkey, size: 40., profile: p } + } + + pub fn size(self, size: f32) -> Self { + Self { size, ..self } + } +} + +impl<'a> Widget for Profile<'a> { + fn ui(self, ui: &mut Ui) -> Response { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 8.; + + let img = self.profile.map_or(None, |f| f.picture().map(|f| Image::from_uri(f))); + ui.add(Avatar::new_optional(img).size(self.size)); + + let name = self.profile.map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f)); + let name = RichText::new(name) + .size(13.) + .color(Color32::WHITE); + ui.add(Label::new(name).wrap_mode(TextWrapMode::Truncate)); + }).response + } +} \ No newline at end of file diff --git a/src/widgets/stream_list.rs b/src/widgets/stream_list.rs index eb401e3..05ef6aa 100644 --- a/src/widgets/stream_list.rs +++ b/src/widgets/stream_list.rs @@ -21,7 +21,7 @@ impl Widget for StreamList<'_> { .show(ui, |ui| { ui.vertical(|ui| { ui.style_mut().spacing.item_spacing = egui::vec2(0., 20.0); - for event in self.streams.iter().take(5) { + for event in self.streams { ui.add(StreamEvent::new(event, self.services)); } }) diff --git a/src/widgets/stream_player.rs b/src/widgets/stream_player.rs index 721d6e8..1232a4b 100644 --- a/src/widgets/stream_player.rs +++ b/src/widgets/stream_player.rs @@ -1,24 +1,29 @@ -use crate::route::RouteServices; use crate::widgets::VideoPlaceholder; -use egui::{Response, Ui, Vec2, Widget}; +use egui::{Context, Response, Ui, Vec2, Widget}; +use egui_video::Player; -pub struct StreamPlayer<'a> { - services: &'a mut RouteServices<'a>, +pub struct StreamPlayer { + player: Option, } -impl<'a> StreamPlayer<'a> { - pub fn new(services: &'a mut RouteServices<'a>) -> Self { - Self { services } +impl StreamPlayer { + pub fn new(ctx: &Context, url: &String) -> Self { + Self { + player: Player::new(ctx, url).map_or(None, |mut f| { + f.start(); + Some(f) + }) + } } } -impl<'a> Widget for StreamPlayer<'a> { +impl Widget for &mut StreamPlayer { fn ui(self, ui: &mut Ui) -> Response { let w = ui.available_width(); let h = w / 16. * 9.; let size = Vec2::new(w, h); - if let Some(p) = self.services.player.as_mut() { + if let Some(mut p) = self.player.as_mut() { p.ui(ui, size) } else { VideoPlaceholder.ui(ui)