feat: profile page
This commit is contained in:
parent
73d03ca0f1
commit
7e9e61d14c
10
src/app.rs
10
src/app.rs
@ -1,5 +1,6 @@
|
||||
use crate::profiles::ProfileLoader;
|
||||
use crate::route::{page, RouteAction, RouteServices, RouteType};
|
||||
use crate::theme::MARGIN_DEFAULT;
|
||||
use crate::widgets::{Header, NostrWidget};
|
||||
use eframe::epaint::{FontFamily, Margin};
|
||||
use eframe::CreationContext;
|
||||
@ -85,6 +86,7 @@ impl notedeck::App for ZapStreamApp {
|
||||
if let Err(e) = ctx.ndb.process_event(ev) {
|
||||
error!("Error processing event: {:?}", e);
|
||||
}
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
RelayMessage::Notice(m) => warn!("Notice from {}: {}", relay, m),
|
||||
}
|
||||
@ -101,8 +103,7 @@ impl notedeck::App for ZapStreamApp {
|
||||
},
|
||||
);
|
||||
|
||||
let mut app_frame = egui::containers::Frame::default();
|
||||
app_frame.inner_margin = self.frame_margin();
|
||||
let app_frame = egui::containers::Frame::default().outer_margin(self.frame_margin());
|
||||
|
||||
// handle app state changes
|
||||
while let Ok(r) = self.routes_rx.try_recv() {
|
||||
@ -125,6 +126,11 @@ impl notedeck::App for ZapStreamApp {
|
||||
RouteType::LoginPage => {
|
||||
self.widget = Box::new(page::LoginPage::new());
|
||||
}
|
||||
RouteType::ProfilePage { link } => {
|
||||
self.widget = Box::new(page::ProfilePage::new(
|
||||
link.id.as_bytes().try_into().unwrap(),
|
||||
));
|
||||
}
|
||||
RouteType::Action { .. } => panic!("Actions!"),
|
||||
_ => panic!("Not implemented"),
|
||||
}
|
||||
|
19
src/link.rs
19
src/link.rs
@ -20,6 +20,15 @@ pub enum IdOrStr {
|
||||
Str(String),
|
||||
}
|
||||
|
||||
impl IdOrStr {
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
IdOrStr::Id(i) => i,
|
||||
IdOrStr::Str(s) => s.as_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for IdOrStr {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@ -85,6 +94,16 @@ impl NostrLink {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn profile(pubkey: &[u8; 32]) -> Self {
|
||||
Self {
|
||||
hrp: NostrLinkType::Profile,
|
||||
id: IdOrStr::Id(*pubkey),
|
||||
kind: None,
|
||||
author: None,
|
||||
relays: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_tag(&self) -> Vec<String> {
|
||||
if self.hrp == NostrLinkType::Coordinate {
|
||||
vec!["a".to_string(), self.to_tag_value()]
|
||||
|
@ -34,7 +34,8 @@ impl NostrWidget for HomePage {
|
||||
let events: Vec<Note> = self
|
||||
.events
|
||||
.iter()
|
||||
.map_while(|n| services.ctx.ndb.get_note_by_key(services.tx, n.key).ok())
|
||||
.filter_map(|n| services.ctx.ndb.get_note_by_key(services.tx, n.key).ok())
|
||||
.filter(|e| e.can_play())
|
||||
.collect();
|
||||
|
||||
let events_live = NotesView::from_vec(
|
||||
@ -68,7 +69,14 @@ impl NostrWidget for HomePage {
|
||||
let events_ended = NotesView::from_vec(
|
||||
events
|
||||
.iter()
|
||||
.filter(|r| matches!(r.status(), StreamStatus::Ended))
|
||||
.filter(|r| {
|
||||
matches!(r.status(), StreamStatus::Ended)
|
||||
&& if let Some(r) = r.recording() {
|
||||
r.len() > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if events_ended.len() > 0 {
|
||||
|
@ -1,9 +1,9 @@
|
||||
use crate::link::NostrLink;
|
||||
use crate::services::ffmpeg_loader::FfmpegLoader;
|
||||
use crate::PollOption;
|
||||
use crate::widgets::PlaceholderRect;
|
||||
use anyhow::{anyhow, bail};
|
||||
use egui::load::SizedTexture;
|
||||
use egui::{Context, Id, Image, TextureHandle};
|
||||
use egui::{Context, Id, Image, ImageSource, TextureHandle, Ui};
|
||||
use ehttp::Response;
|
||||
use enostr::EventClientMessage;
|
||||
use lnurl::lightning_address::LightningAddress;
|
||||
@ -21,13 +21,14 @@ use std::task::Poll;
|
||||
|
||||
mod home;
|
||||
mod login;
|
||||
mod profile;
|
||||
mod stream;
|
||||
|
||||
pub mod page {
|
||||
use crate::route::{home, login, stream};
|
||||
pub use home::HomePage;
|
||||
pub use login::LoginPage;
|
||||
pub use stream::StreamPage;
|
||||
pub use super::home::HomePage;
|
||||
pub use super::login::LoginPage;
|
||||
pub use super::profile::ProfilePage;
|
||||
pub use super::stream::StreamPage;
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
@ -39,7 +40,6 @@ pub enum RouteType {
|
||||
},
|
||||
ProfilePage {
|
||||
link: NostrLink,
|
||||
profile: Option<NoteKey>,
|
||||
},
|
||||
LoginPage,
|
||||
|
||||
@ -117,17 +117,6 @@ impl<'a, 'ctx> RouteServices<'a, 'ctx> {
|
||||
p
|
||||
}
|
||||
|
||||
/// Load image from URL
|
||||
pub fn image<'img, 'b>(&'b mut self, url: &'b str) -> Image<'img> {
|
||||
image_from_cache(self.ctx.img_cache, &self.egui, url)
|
||||
}
|
||||
|
||||
/// Load image from bytes
|
||||
pub fn image_bytes(&self, name: &'static str, data: &'static [u8]) -> Image<'_> {
|
||||
// TODO: loader
|
||||
Image::from_bytes(name, data)
|
||||
}
|
||||
|
||||
/// Create a poll_promise fetch
|
||||
pub fn fetch(&mut self, url: &str) -> Poll<&ehttp::Result<Response>> {
|
||||
if !self.fetch.contains_key(url) {
|
||||
@ -199,14 +188,15 @@ impl<'a, 'ctx> RouteServices<'a, 'ctx> {
|
||||
}
|
||||
|
||||
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> {
|
||||
|
||||
pub fn image_from_cache<'a>(img_cache: &mut ImageCache, ui: &Ui, url: &str) -> Image<'a> {
|
||||
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);
|
||||
let fetch = fetch_img(img_cache, ui.ctx(), url);
|
||||
img_cache.map_mut().insert(url.to_string(), fetch);
|
||||
Image::from_bytes(url.to_string(), &BLACK_PIXEL)
|
||||
}
|
||||
|
85
src/route/profile.rs
Normal file
85
src/route/profile.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use crate::note_ref::NoteRef;
|
||||
use crate::note_view::NotesView;
|
||||
use crate::route::{image_from_cache, RouteServices};
|
||||
use crate::sub::SubRef;
|
||||
use crate::theme::{MARGIN_DEFAULT, ROUNDING_DEFAULT};
|
||||
use crate::widgets::{sub_or_poll, NostrWidget, PlaceholderRect, Profile, StreamList};
|
||||
use egui::{vec2, Frame, Id, Response, ScrollArea, Ui, Widget};
|
||||
use nostrdb::{Filter, Note};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct ProfilePage {
|
||||
pubkey: [u8; 32],
|
||||
events: HashSet<NoteRef>,
|
||||
sub: Option<SubRef>,
|
||||
}
|
||||
|
||||
impl ProfilePage {
|
||||
pub fn new(pubkey: [u8; 32]) -> Self {
|
||||
Self {
|
||||
pubkey,
|
||||
events: HashSet::new(),
|
||||
sub: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NostrWidget for ProfilePage {
|
||||
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response {
|
||||
let profile = services.profile(&self.pubkey);
|
||||
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
Frame::default()
|
||||
.inner_margin(MARGIN_DEFAULT)
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 8.0;
|
||||
|
||||
if let Some(banner) = profile.map(|p| p.banner()).flatten() {
|
||||
image_from_cache(&mut services.ctx.img_cache, ui, banner)
|
||||
.fit_to_exact_size(vec2(ui.available_width(), 360.0))
|
||||
.rounding(ROUNDING_DEFAULT)
|
||||
.ui(ui);
|
||||
} else {
|
||||
ui.add(PlaceholderRect);
|
||||
}
|
||||
Profile::from_profile(&self.pubkey, &profile)
|
||||
.size(88.0)
|
||||
.render(ui, services);
|
||||
});
|
||||
|
||||
let events: Vec<Note> = self
|
||||
.events
|
||||
.iter()
|
||||
.filter_map(|e| services.ctx.ndb.get_note_by_key(services.tx, e.key).ok())
|
||||
.collect();
|
||||
|
||||
StreamList::new(
|
||||
Id::from("profile-streams"),
|
||||
NotesView::from_vec(events.iter().collect()),
|
||||
Some("Past Streams"),
|
||||
)
|
||||
.render(ui, services);
|
||||
});
|
||||
ui.response()
|
||||
}
|
||||
|
||||
fn update(&mut self, services: &mut RouteServices<'_, '_>) -> anyhow::Result<()> {
|
||||
sub_or_poll(
|
||||
services.ctx.ndb,
|
||||
services.tx,
|
||||
services.ctx.pool,
|
||||
&mut self.events,
|
||||
&mut self.sub,
|
||||
vec![
|
||||
Filter::new()
|
||||
.kinds([30_311])
|
||||
.authors(&[self.pubkey])
|
||||
.build(),
|
||||
Filter::new()
|
||||
.kinds([30_311])
|
||||
.pubkeys(&[self.pubkey])
|
||||
.build(),
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
@ -152,7 +152,7 @@ impl NostrWidget for StreamPage {
|
||||
.collect();
|
||||
|
||||
if let Some(event) = events.first() {
|
||||
if let Some(stream) = event.stream() {
|
||||
if let Some(stream) = event.streaming() {
|
||||
if self.player.is_none() {
|
||||
let p = StreamPlayer::new(ui.ctx(), &stream.to_string());
|
||||
self.player = Some(p);
|
||||
|
@ -26,7 +26,10 @@ pub trait StreamInfo {
|
||||
|
||||
fn host(&self) -> &[u8; 32];
|
||||
|
||||
fn stream(&self) -> Option<&str>;
|
||||
fn streaming(&self) -> Option<&str>;
|
||||
|
||||
fn recording(&self) -> Option<&str>;
|
||||
|
||||
/// Is the stream playable by this app
|
||||
fn can_play(&self) -> bool;
|
||||
|
||||
@ -68,7 +71,7 @@ impl StreamInfo for Note<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn stream(&self) -> Option<&str> {
|
||||
fn streaming(&self) -> Option<&str> {
|
||||
if let Some(s) = self.get_tag_value("streaming") {
|
||||
s.variant().str()
|
||||
} else {
|
||||
@ -76,9 +79,17 @@ impl StreamInfo for Note<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn recording(&self) -> Option<&str> {
|
||||
if let Some(s) = self.get_tag_value("recording") {
|
||||
s.variant().str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the stream playable by this app
|
||||
fn can_play(&self) -> bool {
|
||||
if let Some(stream) = self.stream() {
|
||||
if let Some(stream) = self.streaming() {
|
||||
stream.contains(".m3u8")
|
||||
} else {
|
||||
false
|
||||
|
@ -1,6 +1,7 @@
|
||||
use egui::{Color32, Margin};
|
||||
|
||||
pub const FONT_SIZE: f32 = 13.0;
|
||||
pub const FONT_SIZE_SM: f32 = FONT_SIZE * 0.8;
|
||||
pub const FONT_SIZE_LG: f32 = FONT_SIZE * 1.5;
|
||||
pub const ROUNDING_DEFAULT: f32 = 12.0;
|
||||
pub const MARGIN_DEFAULT: Margin = Margin::symmetric(12., 6.);
|
||||
|
@ -53,13 +53,15 @@ impl Avatar {
|
||||
pub fn render(self, ui: &mut Ui, img_cache: &mut ImageCache) -> Response {
|
||||
let size_v = self.size.unwrap_or(40.);
|
||||
let size = Vec2::new(size_v, size_v);
|
||||
if !ui.is_visible() {
|
||||
if !ui.is_rect_visible(ui.cursor()) {
|
||||
return Self::placeholder(ui, size_v);
|
||||
}
|
||||
match &self.image {
|
||||
Some(img) => image_from_cache(img_cache, ui.ctx(), img)
|
||||
Some(img) => image_from_cache(img_cache, ui, img)
|
||||
.max_size(size)
|
||||
.fit_to_exact_size(size)
|
||||
.rounding(Rounding::same(size_v))
|
||||
.sense(Sense::click())
|
||||
.ui(ui),
|
||||
None => Self::placeholder(ui, size_v),
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
use crate::link::NostrLink;
|
||||
use crate::route::{RouteServices, RouteType};
|
||||
use crate::widgets::avatar::Avatar;
|
||||
use crate::widgets::Button;
|
||||
use eframe::emath::Align;
|
||||
use eframe::epaint::Vec2;
|
||||
use egui::{CursorIcon, Frame, Layout, Margin, Response, Sense, Ui, Widget};
|
||||
use egui::{CursorIcon, Frame, Image, Layout, Margin, Response, Sense, Ui, Widget};
|
||||
use nostrdb::Transaction;
|
||||
|
||||
pub struct Header;
|
||||
@ -27,8 +28,7 @@ impl Header {
|
||||
Layout::left_to_right(Align::Center),
|
||||
|ui| {
|
||||
ui.style_mut().spacing.item_spacing.x = 16.;
|
||||
if services
|
||||
.image_bytes("logo.svg", logo_bytes)
|
||||
if Image::from_bytes("logo.svg", logo_bytes)
|
||||
.max_height(24.)
|
||||
.sense(Sense::click())
|
||||
.ui(ui)
|
||||
@ -40,8 +40,14 @@ impl 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, tx)
|
||||
.render(ui, services.ctx.img_cache);
|
||||
if Avatar::pubkey(&acc.pubkey, services.ctx.ndb, tx)
|
||||
.render(ui, services.ctx.img_cache)
|
||||
.clicked()
|
||||
{
|
||||
services.navigate(RouteType::ProfilePage {
|
||||
link: NostrLink::profile(acc.pubkey.bytes()),
|
||||
})
|
||||
}
|
||||
} else if Button::new().show(ui, |ui| ui.label("Login")).clicked() {
|
||||
services.navigate(RouteType::LoginPage);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ mod chat;
|
||||
mod chat_message;
|
||||
mod chat_zap;
|
||||
mod header;
|
||||
mod pill;
|
||||
mod placeholder_rect;
|
||||
mod profile;
|
||||
mod stream_list;
|
||||
@ -64,6 +65,7 @@ pub use self::avatar::Avatar;
|
||||
pub use self::button::Button;
|
||||
pub use self::chat::Chat;
|
||||
pub use self::header::Header;
|
||||
pub use self::pill::Pill;
|
||||
pub use self::placeholder_rect::PlaceholderRect;
|
||||
pub use self::profile::Profile;
|
||||
pub use self::stream_list::StreamList;
|
||||
|
35
src/widgets/pill.rs
Normal file
35
src/widgets/pill.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use crate::theme::{FONT_SIZE, NEUTRAL_800};
|
||||
use eframe::epaint::Margin;
|
||||
use egui::{Color32, Frame, Response, RichText, Ui, Widget};
|
||||
|
||||
pub struct Pill {
|
||||
text: String,
|
||||
color: Color32,
|
||||
}
|
||||
|
||||
impl Pill {
|
||||
pub fn new(text: &str) -> Self {
|
||||
Self {
|
||||
text: String::from(text),
|
||||
color: NEUTRAL_800,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color32) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Pill {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
Frame::default()
|
||||
.inner_margin(Margin::symmetric(5.0, 3.0))
|
||||
.rounding(5.0)
|
||||
.fill(self.color)
|
||||
.show(ui, |ui| {
|
||||
ui.label(RichText::new(&self.text).size(FONT_SIZE));
|
||||
})
|
||||
.response
|
||||
}
|
||||
}
|
@ -2,15 +2,29 @@ use crate::route::RouteServices;
|
||||
use crate::theme::FONT_SIZE;
|
||||
use crate::widgets::{Avatar, Username};
|
||||
use egui::{Response, Ui};
|
||||
use nostrdb::NdbProfile;
|
||||
|
||||
pub struct Profile<'a> {
|
||||
size: f32,
|
||||
pubkey: &'a [u8; 32],
|
||||
profile: &'a Option<NdbProfile<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Profile<'a> {
|
||||
pub fn new(pubkey: &'a [u8; 32]) -> Self {
|
||||
Self { pubkey, size: 40. }
|
||||
Self {
|
||||
pubkey,
|
||||
size: 40.,
|
||||
profile: &None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_profile(pubkey: &'a [u8; 32], profile: &'a Option<NdbProfile<'a>>) -> Self {
|
||||
Self {
|
||||
pubkey,
|
||||
profile,
|
||||
size: 40.,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(self, size: f32) -> Self {
|
||||
@ -21,7 +35,11 @@ impl<'a> Profile<'a> {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 8.;
|
||||
|
||||
let profile = services.profile(self.pubkey);
|
||||
let profile = if let Some(profile) = self.profile {
|
||||
Some(*profile)
|
||||
} else {
|
||||
services.profile(self.pubkey)
|
||||
};
|
||||
Avatar::from_profile(&profile)
|
||||
.size(self.size)
|
||||
.render(ui, services.ctx.img_cache);
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::note_view::NotesView;
|
||||
use crate::route::RouteServices;
|
||||
use crate::stream_info::StreamInfo;
|
||||
use crate::theme::MARGIN_DEFAULT;
|
||||
use crate::widgets::stream_tile::StreamEvent;
|
||||
use egui::{vec2, Frame, Grid, Margin, Response, Ui, WidgetText};
|
||||
use itertools::Itertools;
|
||||
@ -35,9 +36,8 @@ impl<'a> StreamList<'a> {
|
||||
};
|
||||
|
||||
let grid_padding = 20.;
|
||||
let frame_margin = 16.0;
|
||||
Frame::none()
|
||||
.inner_margin(Margin::symmetric(frame_margin, 0.))
|
||||
.inner_margin(MARGIN_DEFAULT)
|
||||
.show(ui, |ui| {
|
||||
let grid_spacing_consumed = (cols - 1) as f32 * grid_padding;
|
||||
let g_w = (ui.available_width() - grid_spacing_consumed) / cols as f32;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::link::NostrLink;
|
||||
use crate::route::{RouteServices, RouteType};
|
||||
use crate::route::{image_from_cache, RouteServices, RouteType};
|
||||
use crate::stream_info::{StreamInfo, StreamStatus};
|
||||
use crate::theme::{NEUTRAL_800, NEUTRAL_900, PRIMARY, ROUNDING_DEFAULT};
|
||||
use crate::widgets::avatar::Avatar;
|
||||
@ -33,8 +33,10 @@ impl<'a> StreamEvent<'a> {
|
||||
|
||||
let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::click());
|
||||
|
||||
let cover = if ui.is_visible() {
|
||||
self.event.image().map(|p| services.image(p))
|
||||
let cover = if ui.is_rect_visible(response.rect) {
|
||||
self.event
|
||||
.image()
|
||||
.map(|p| image_from_cache(services.ctx.img_cache, ui, p))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -1,10 +1,11 @@
|
||||
use crate::note_util::NoteUtil;
|
||||
use crate::route::RouteServices;
|
||||
use crate::stream_info::StreamInfo;
|
||||
use crate::theme::MARGIN_DEFAULT;
|
||||
use crate::stream_info::{StreamInfo, StreamStatus};
|
||||
use crate::theme::{MARGIN_DEFAULT, NEUTRAL_900, PRIMARY};
|
||||
use crate::widgets::zap::ZapButton;
|
||||
use crate::widgets::Pill;
|
||||
use crate::widgets::Profile;
|
||||
use egui::{Color32, Frame, Label, Response, RichText, TextWrapMode, Ui};
|
||||
use egui::{vec2, Color32, Frame, Label, Response, RichText, TextWrapMode, Ui};
|
||||
use nostrdb::Note;
|
||||
|
||||
pub struct StreamTitle<'a> {
|
||||
@ -19,6 +20,8 @@ impl<'a> StreamTitle<'a> {
|
||||
Frame::none()
|
||||
.outer_margin(MARGIN_DEFAULT)
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(5., 8.0);
|
||||
|
||||
let title = RichText::new(self.event.title().unwrap_or("Untitled"))
|
||||
.size(20.)
|
||||
.color(Color32::WHITE);
|
||||
@ -31,6 +34,20 @@ impl<'a> StreamTitle<'a> {
|
||||
ZapButton::event(self.event).render(ui, services);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let status = self.event.status().to_string().to_uppercase();
|
||||
let live_label_color = if self.event.status() == StreamStatus::Live {
|
||||
PRIMARY
|
||||
} else {
|
||||
NEUTRAL_900
|
||||
};
|
||||
ui.add(Pill::new(&status).color(live_label_color));
|
||||
|
||||
ui.add(Pill::new(&format!(
|
||||
"{} viewers",
|
||||
self.event.viewers().unwrap_or(0)
|
||||
)));
|
||||
});
|
||||
if let Some(summary) = self
|
||||
.event
|
||||
.get_tag_value("summary")
|
||||
|
@ -3,7 +3,7 @@ use crate::route::RouteServices;
|
||||
use crate::theme::{MARGIN_DEFAULT, NEUTRAL_900, ROUNDING_DEFAULT};
|
||||
use crate::widgets::NativeTextInput;
|
||||
use eframe::emath::Align;
|
||||
use egui::{Frame, Layout, Response, Sense, Ui, Widget};
|
||||
use egui::{Frame, Image, Layout, Response, Sense, Ui, Widget};
|
||||
use log::info;
|
||||
|
||||
pub struct WriteChat {
|
||||
@ -28,8 +28,7 @@ impl WriteChat {
|
||||
.rounding(ROUNDING_DEFAULT)
|
||||
.show(ui, |ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if services
|
||||
.image_bytes("send-03.svg", logo_bytes)
|
||||
if Image::from_bytes("send-03.svg", logo_bytes)
|
||||
.sense(Sense::click())
|
||||
.ui(ui)
|
||||
.clicked()
|
||||
|
@ -7,7 +7,8 @@ use crate::theme::{
|
||||
use crate::widgets::{Button, NativeTextInput};
|
||||
use crate::zap::format_sats;
|
||||
use anyhow::{anyhow, bail};
|
||||
use egui::{vec2, Frame, Grid, Response, RichText, Stroke, Ui, Widget};
|
||||
use egui::text::{LayoutJob, TextWrapping};
|
||||
use egui::{vec2, Frame, Grid, Response, RichText, Stroke, TextFormat, TextWrapMode, Ui, Widget};
|
||||
use egui_modal::Modal;
|
||||
use egui_qr::QrCodeWidget;
|
||||
use enostr::PoolRelay;
|
||||
@ -109,10 +110,17 @@ impl<'a> ZapButton<'a> {
|
||||
}
|
||||
ZapState::Invoice { invoice } => {
|
||||
if let Ok(q) = QrCodeWidget::from_data(invoice.pr.as_bytes()) {
|
||||
ui.add_sized(vec2(256., 256.), q);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_sized(vec2(256., 256.), q);
|
||||
|
||||
let rt = RichText::new(&invoice.pr).code();
|
||||
ui.label(rt);
|
||||
let mut job = LayoutJob::default();
|
||||
job.wrap = TextWrapping::from_wrap_mode_and_width(
|
||||
TextWrapMode::Truncate,
|
||||
ui.available_width(),
|
||||
);
|
||||
job.append(&invoice.pr, 0.0, TextFormat::default());
|
||||
ui.label(job);
|
||||
});
|
||||
}
|
||||
}
|
||||
ZapState::Error(e) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user