wip
This commit is contained in:
parent
f7021094bc
commit
dc7fee4151
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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
21
src/sub.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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,10 +60,22 @@ 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)
|
||||||
{
|
{
|
||||||
|
match ev.kind() {
|
||||||
|
1311 => {
|
||||||
let profile = services.profile(ev.pubkey());
|
let profile = services.profile(ev.pubkey());
|
||||||
ChatMessage::new(&stream, &ev, &profile)
|
ChatMessage::new(&stream, &ev, &profile)
|
||||||
.render(ui, services.ctx.img_cache);
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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
68
src/widgets/chat_zap.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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(¬e));
|
store.insert(NoteRef::from_note(¬e));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} 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
56
src/zap.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user