This commit is contained in:
kieran 2025-01-09 14:26:47 +00:00
parent f7021094bc
commit dc7fee4151
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
10 changed files with 185 additions and 15 deletions

View File

@ -9,8 +9,10 @@ mod profiles;
mod route; mod route;
mod services; mod services;
mod stream_info; mod stream_info;
mod sub;
mod theme; mod theme;
mod widgets; mod widgets;
mod zap;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
use android_activity::AndroidApp; use android_activity::AndroidApp;

View File

@ -2,15 +2,16 @@ use crate::note_ref::NoteRef;
use crate::note_view::NotesView; use crate::note_view::NotesView;
use crate::route::RouteServices; use crate::route::RouteServices;
use crate::stream_info::{StreamInfo, StreamStatus}; use crate::stream_info::{StreamInfo, StreamStatus};
use crate::sub::SubRef;
use crate::widgets; use crate::widgets;
use crate::widgets::{sub_or_poll, NostrWidget}; use crate::widgets::{sub_or_poll, NostrWidget};
use egui::{Id, Response, RichText, ScrollArea, Ui}; use egui::{Id, Response, RichText, ScrollArea, Ui};
use nostrdb::{Filter, Note, Subscription}; use nostrdb::{Filter, Note};
use std::collections::HashSet; use std::collections::HashSet;
pub struct HomePage { pub struct HomePage {
events: HashSet<NoteRef>, events: HashSet<NoteRef>,
sub: Option<Subscription>, sub: Option<SubRef>,
} }
impl HomePage { impl HomePage {

View File

@ -6,9 +6,10 @@ use crate::widgets::{
sub_or_poll, Chat, NostrWidget, PlaceholderRect, StreamPlayer, StreamTitle, WriteChat, sub_or_poll, Chat, NostrWidget, PlaceholderRect, StreamPlayer, StreamTitle, WriteChat,
}; };
use egui::{vec2, Align, Frame, Layout, Response, Stroke, Ui, Vec2, Widget}; use egui::{vec2, Align, Frame, Layout, Response, Stroke, Ui, Vec2, Widget};
use nostrdb::{Filter, Note, Subscription}; use nostrdb::{Filter, Note};
use crate::note_ref::NoteRef; use crate::note_ref::NoteRef;
use crate::sub::SubRef;
use std::borrow::Borrow; use std::borrow::Borrow;
use std::collections::HashSet; use std::collections::HashSet;
@ -19,7 +20,7 @@ pub struct StreamPage {
new_msg: WriteChat, new_msg: WriteChat,
events: HashSet<NoteRef>, events: HashSet<NoteRef>,
sub: Option<Subscription>, sub: Option<SubRef>,
} }
impl StreamPage { impl StreamPage {

21
src/sub.rs Normal file
View File

@ -0,0 +1,21 @@
use log::info;
use nostrdb::{Ndb, Subscription};
pub struct SubRef {
pub sub: Subscription,
ndb: Ndb,
}
impl SubRef {
pub fn new(sub: Subscription, ndb: Ndb) -> Self {
info!("Creating sub: {}", sub.id());
SubRef { sub, ndb }
}
}
impl Drop for SubRef {
fn drop(&mut self) {
self.ndb.unsubscribe(self.sub).expect("unsubscribe failed");
info!("Closing sub: {}", self.sub.id());
}
}

View File

@ -7,3 +7,4 @@ pub const PRIMARY: Color32 = Color32::from_rgb(248, 56, 217);
pub const NEUTRAL_500: Color32 = Color32::from_rgb(115, 115, 115); pub const NEUTRAL_500: Color32 = Color32::from_rgb(115, 115, 115);
pub const NEUTRAL_800: Color32 = Color32::from_rgb(38, 38, 38); pub const NEUTRAL_800: Color32 = Color32::from_rgb(38, 38, 38);
pub const NEUTRAL_900: Color32 = Color32::from_rgb(23, 23, 23); pub const NEUTRAL_900: Color32 = Color32::from_rgb(23, 23, 23);
pub const ZAP: Color32 = Color32::from_rgb(255, 141, 43);

View File

@ -1,18 +1,21 @@
use crate::link::NostrLink; use crate::link::NostrLink;
use crate::note_ref::NoteRef; use crate::note_ref::NoteRef;
use crate::route::RouteServices; use crate::route::RouteServices;
use crate::sub::SubRef;
use crate::widgets::chat_message::ChatMessage; use crate::widgets::chat_message::ChatMessage;
use crate::widgets::chat_zap::ChatZap;
use crate::widgets::{sub_or_poll, NostrWidget}; use crate::widgets::{sub_or_poll, NostrWidget};
use crate::zap::Zap;
use egui::{Frame, Margin, Response, ScrollArea, Ui}; use egui::{Frame, Margin, Response, ScrollArea, Ui};
use itertools::Itertools; use itertools::Itertools;
use nostrdb::{Filter, NoteKey, Subscription}; use nostrdb::{Filter, NoteKey};
use std::collections::HashSet; use std::collections::HashSet;
pub struct Chat { pub struct Chat {
link: NostrLink, link: NostrLink,
stream: NoteKey, stream: NoteKey,
events: HashSet<NoteRef>, events: HashSet<NoteRef>,
sub: Option<Subscription>, sub: Option<SubRef>,
} }
impl Chat { impl Chat {
@ -27,7 +30,7 @@ impl Chat {
pub fn get_filter(&self) -> Filter { pub fn get_filter(&self) -> Filter {
Filter::new() Filter::new()
.kinds([1_311]) .kinds([1_311, 9_735])
.tags([self.link.to_tag_value()], 'a') .tags([self.link.to_tag_value()], 'a')
.build() .build()
} }
@ -57,9 +60,21 @@ impl NostrWidget for Chat {
if let Ok(ev) = 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)
{ {
let profile = services.profile(ev.pubkey()); match ev.kind() {
ChatMessage::new(&stream, &ev, &profile) 1311 => {
.render(ui, services.ctx.img_cache); let profile = services.profile(ev.pubkey());
ChatMessage::new(&stream, &ev, &profile)
.render(ui, services.ctx.img_cache);
}
9735 => {
if let Ok(zap) = Zap::from_receipt(ev) {
let profile = services.profile(&zap.sender);
ChatZap::new(&zap, &profile)
.render(ui, services.ctx.img_cache);
}
}
_ => {}
}
} }
} }
}) })

View File

@ -26,7 +26,7 @@ impl<'a> ChatMessage<'a> {
} }
} }
pub fn render(&mut self, ui: &mut Ui, img_cache: &mut ImageCache) -> Response { pub fn render(self, ui: &mut Ui, img_cache: &mut ImageCache) -> Response {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
// TODO: avoid this somehow // TODO: avoid this somehow
@ -52,6 +52,9 @@ impl<'a> ChatMessage<'a> {
.size(24.) .size(24.)
.render(ui, img_cache); .render(ui, img_cache);
ui.add(Label::new(job).wrap_mode(TextWrapMode::Wrap)); ui.add(Label::new(job).wrap_mode(TextWrapMode::Wrap));
// consume reset of space
ui.add_space(ui.available_size_before_wrap().x);
}) })
.response .response
} }

68
src/widgets/chat_zap.rs Normal file
View File

@ -0,0 +1,68 @@
use crate::theme::{MARGIN_DEFAULT, ROUNDING_DEFAULT, ZAP};
use crate::widgets::Avatar;
use crate::zap::Zap;
use eframe::emath::Align;
use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapMode};
use eframe::epaint::Color32;
use egui::{Frame, Label, Response, Stroke, Ui};
use nostrdb::NdbProfile;
use notedeck::ImageCache;
pub struct ChatZap<'a> {
zap: &'a Zap<'a>,
profile: &'a Option<NdbProfile<'a>>,
}
impl<'a> ChatZap<'a> {
pub fn new(zap: &'a Zap, profile: &'a Option<NdbProfile<'a>>) -> Self {
Self { zap, profile }
}
pub fn render(self, ui: &mut Ui, img_cache: &mut ImageCache) -> Response {
Frame::default()
.rounding(ROUNDING_DEFAULT)
.inner_margin(MARGIN_DEFAULT)
.stroke(Stroke::new(1., ZAP))
.show(ui, |ui| {
ui.horizontal_wrapped(|ui| {
let mut job = LayoutJob::default();
// TODO: avoid this somehow
job.wrap.break_anywhere = true;
let name = self
.profile
.map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f));
let mut format = TextFormat::default();
format.line_height = Some(24.0);
format.valign = Align::Center;
format.color = ZAP;
job.append(name, 0.0, format.clone());
format.color = Color32::WHITE;
job.append("zapped", 5.0, format.clone());
format.color = ZAP;
job.append(
(self.zap.amount / 1000).to_string().as_str(),
5.0,
format.clone(),
);
format.color = Color32::WHITE;
job.append("sats", 5.0, format.clone());
if !self.zap.message.is_empty() {
job.append(&format!("\n{}", self.zap.message), 0.0, format.clone());
}
Avatar::from_profile(&self.profile)
.size(24.)
.render(ui, img_cache);
ui.add(Label::new(job).wrap_mode(TextWrapMode::Wrap));
// consume reset of space
ui.add_space(ui.available_size_before_wrap().x);
});
})
.response
}
}

View File

@ -2,6 +2,7 @@ mod avatar;
mod button; mod button;
mod chat; mod chat;
mod chat_message; mod chat_message;
mod chat_zap;
mod header; mod header;
mod placeholder_rect; mod placeholder_rect;
mod profile; mod profile;
@ -15,9 +16,10 @@ mod write_chat;
use crate::note_ref::NoteRef; use crate::note_ref::NoteRef;
use crate::route::RouteServices; use crate::route::RouteServices;
use crate::sub::SubRef;
use egui::{Response, Ui}; use egui::{Response, Ui};
use enostr::RelayPool; use enostr::RelayPool;
use nostrdb::{Filter, Ndb, Subscription, Transaction}; use nostrdb::{Filter, Ndb, Transaction};
use std::collections::HashSet; use std::collections::HashSet;
/// A stateful widget which requests nostr data /// A stateful widget which requests nostr data
@ -35,18 +37,18 @@ pub fn sub_or_poll(
tx: &Transaction, tx: &Transaction,
pool: &mut RelayPool, pool: &mut RelayPool,
store: &mut HashSet<NoteRef>, store: &mut HashSet<NoteRef>,
sub: &mut Option<Subscription>, sub: &mut Option<SubRef>,
filters: Vec<Filter>, filters: Vec<Filter>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if let Some(sub) = sub { if let Some(sub) = sub {
ndb.poll_for_notes(*sub, 500).into_iter().for_each(|e| { ndb.poll_for_notes(sub.sub, 500).into_iter().for_each(|e| {
if let Ok(note) = ndb.get_note_by_key(tx, e) { if let Ok(note) = ndb.get_note_by_key(tx, e) {
store.insert(NoteRef::from_note(&note)); store.insert(NoteRef::from_note(&note));
} }
}); });
} else { } else {
let s = ndb.subscribe(filters.as_slice())?; let s = ndb.subscribe(filters.as_slice())?;
sub.replace(s); sub.replace(SubRef::new(s, ndb.clone()));
ndb.query(tx, filters.as_slice(), 500)? ndb.query(tx, filters.as_slice(), 500)?
.into_iter() .into_iter()
.for_each(|e| { .for_each(|e| {

56
src/zap.rs Normal file
View File

@ -0,0 +1,56 @@
use crate::note_util::NoteUtil;
use anyhow::{anyhow, bail, Result};
use nostr::{Event, JsonUtil, Kind, TagStandard};
use nostrdb::Note;
pub struct Zap<'a> {
pub sender: [u8; 32],
pub receiver: [u8; 32],
pub zapper_service: &'a [u8; 32],
pub amount: u64,
pub message: String,
}
impl<'a> Zap<'a> {
pub fn from_receipt(event: Note<'a>) -> Result<Zap> {
if event.kind() != 9735 {
bail!("not a zap receipt");
}
let req_json = event
.get_tag_value("description")
.ok_or(anyhow!("missing description"))?;
let req = Event::from_json(
req_json
.variant()
.str()
.ok_or(anyhow!("empty description"))?,
)?;
if req.kind != Kind::ZapRequest {
bail!("not a zap request");
}
let dest = req
.tags
.iter()
.find_map(|t| match t.as_standardized() {
Some(TagStandard::PublicKey { public_key, .. }) => Some(public_key.to_bytes()),
_ => None,
})
.ok_or(anyhow!("missing p tag in zap request"))?;
let amount = req.tags.iter().find_map(|t| match t.as_standardized() {
Some(TagStandard::Amount { millisats, .. }) => Some(*millisats),
_ => None,
});
Ok(Zap {
sender: req.pubkey.to_bytes(),
receiver: dest,
zapper_service: event.pubkey(),
amount: amount.unwrap_or(0u64),
message: req.content,
})
}
}