feat: chat
feat: android setup
This commit is contained in:
parent
f0a50918a8
commit
6017ce18d4
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
/lock.mdb
|
||||
/data.mdb
|
||||
/.idea
|
||||
/cache
|
113
Cargo.lock
generated
113
Cargo.lock
generated
@ -127,6 +127,23 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_log-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937"
|
||||
|
||||
[[package]]
|
||||
name = "android_logger"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826"
|
||||
dependencies = [
|
||||
"android_log-sys",
|
||||
"env_filter",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@ -1177,6 +1194,16 @@ dependencies = [
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
@ -1814,6 +1841,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-native-certs 0.8.0",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
@ -2850,6 +2878,12 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.47"
|
||||
@ -3367,6 +3401,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-native-certs 0.7.3",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
@ -3494,6 +3529,32 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.1.3"
|
||||
@ -3545,6 +3606,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
@ -3569,6 +3639,19 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sctk-adwaita"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"log",
|
||||
"memmap2",
|
||||
"smithay-client-toolkit",
|
||||
"tiny-skia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sdl2"
|
||||
version = "0.37.0"
|
||||
@ -3614,6 +3697,29 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.23"
|
||||
@ -5146,6 +5252,7 @@ dependencies = [
|
||||
"raw-window-handle",
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix",
|
||||
"sctk-adwaita",
|
||||
"smithay-client-toolkit",
|
||||
"smol_str",
|
||||
"tracing",
|
||||
@ -5246,6 +5353,8 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
name = "zap_stream_app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"android-activity",
|
||||
"android_logger",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bech32",
|
||||
@ -5256,13 +5365,17 @@ dependencies = [
|
||||
"egui_extras",
|
||||
"egui_inbox",
|
||||
"image",
|
||||
"itertools 0.13.0",
|
||||
"libc",
|
||||
"log",
|
||||
"nostr-sdk",
|
||||
"nostrdb",
|
||||
"pretty_env_logger",
|
||||
"reqwest",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"uuid",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
30
Cargo.toml
30
Cargo.toml
@ -3,9 +3,11 @@ name = "zap_stream_app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = "1.40.0"
|
||||
[lib]
|
||||
crate-type = ["lib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.40.0", features = ["fs", "rt-multi-thread", "rt"] }
|
||||
egui = { version = "0.29.1" }
|
||||
eframe = { version = "0.29.1", default-features = false, features = ["glow", "wgpu", "wayland", "x11", "android-native-activity"] }
|
||||
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", version = "0.3.4" }
|
||||
@ -22,3 +24,27 @@ uuid = { version = "1.11.0", features = ["v4"] }
|
||||
chrono = "0.4.38"
|
||||
anyhow = "1.0.89"
|
||||
async-trait = "0.1.83"
|
||||
sha2 = "0.10.8"
|
||||
reqwest = { version = "0.12.7", default-features = false, features = [ "rustls-tls-native-roots" ] }
|
||||
itertools = "0.13.0"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.14.1"
|
||||
android-activity = { version = "0.6.0", features = [ "native-activity" ] }
|
||||
winit = { version = "0.30.5", features = [ "android-native-activity" ] }
|
||||
|
||||
[package.metadata.android]
|
||||
package = "stream.zap.app"
|
||||
build_targets = [ "aarch64-linux-android" ]
|
||||
#build_targets = [ "armv7-linux-androideabi" ]
|
||||
large_heap = true
|
||||
|
||||
[package.metadata.android.sdk]
|
||||
min_sdk_version = 20
|
||||
target_sdk_version = 32
|
||||
|
||||
[package.metadata.android.application]
|
||||
extract_native_libs = true
|
||||
|
||||
[package.metadata.android.application.activity]
|
||||
config_changes = "orientation"
|
@ -1,6 +1,6 @@
|
||||
use crate::route::Router;
|
||||
use eframe::{App, CreationContext, Frame};
|
||||
use egui::{Color32, Context};
|
||||
use egui::{Color32, Context, Pos2, Rect, Rounding};
|
||||
use nostr_sdk::database::MemoryDatabase;
|
||||
use nostr_sdk::{Client, RelayPoolNotification};
|
||||
use nostrdb::{Config, Ndb};
|
||||
@ -53,6 +53,8 @@ impl App for ZapStreamApp {
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(app_frame)
|
||||
.show(ctx, |ui| self.router.show(ui));
|
||||
.show(ctx, |ui| {
|
||||
self.router.show(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,6 @@
|
||||
use crate::app::ZapStreamApp;
|
||||
use eframe::Renderer;
|
||||
use egui::Vec2;
|
||||
|
||||
mod app;
|
||||
mod link;
|
||||
mod note_util;
|
||||
mod route;
|
||||
mod services;
|
||||
mod stream_info;
|
||||
pub mod widgets;
|
||||
use zap_stream_app::app::ZapStreamApp;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
39
src/lib.rs
Normal file
39
src/lib.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use crate::app::ZapStreamApp;
|
||||
use eframe::Renderer;
|
||||
use egui::Vec2;
|
||||
|
||||
pub mod app;
|
||||
mod link;
|
||||
mod note_util;
|
||||
mod route;
|
||||
mod services;
|
||||
mod stream_info;
|
||||
pub mod widgets;
|
||||
pub mod theme;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use winit::platform::android::activity::AndroidApp;
|
||||
#[cfg(target_os = "android")]
|
||||
use winit::platform::android::EventLoopBuilderExtAndroid;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[no_mangle]
|
||||
#[tokio::main]
|
||||
pub async fn android_main(app: AndroidApp) {
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
|
||||
|
||||
let mut options = eframe::NativeOptions::default();
|
||||
options.renderer = Renderer::Glow;
|
||||
options.viewport = options.viewport.with_inner_size(Vec2::new(360., 720.));
|
||||
let app_clone_for_event_loop = app.clone();
|
||||
options.event_loop_builder = Some(Box::new(move |builder| {
|
||||
builder.with_android_app(app_clone_for_event_loop);
|
||||
}));
|
||||
|
||||
let _res = eframe::run_native(
|
||||
"zap.stream",
|
||||
options,
|
||||
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc)))),
|
||||
);
|
||||
}
|
@ -112,6 +112,8 @@ impl TryInto<Filter> for &NostrLink {
|
||||
fn try_into(self) -> Result<Filter, Self::Error> {
|
||||
match self.hrp {
|
||||
NostrLinkType::Coordinate => Ok(Filter::new()
|
||||
.kinds([self.kind.unwrap() as u64])
|
||||
.authors([&self.author.unwrap()])
|
||||
.tags(
|
||||
[match self.id {
|
||||
IdOrStr::Str(ref s) => s.to_owned(),
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
3
src/resources/send-03.svg
Normal file
3
src/resources/send-03.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5004 11.9998H5.00043M4.91577 12.2913L2.58085 19.266C2.39742 19.8139 2.3057 20.0879 2.37152 20.2566C2.42868 20.4031 2.55144 20.5142 2.70292 20.5565C2.87736 20.6052 3.14083 20.4866 3.66776 20.2495L20.3792 12.7293C20.8936 12.4979 21.1507 12.3822 21.2302 12.2214C21.2993 12.0817 21.2993 11.9179 21.2302 11.7782C21.1507 11.6174 20.8936 11.5017 20.3792 11.2703L3.66193 3.74751C3.13659 3.51111 2.87392 3.39291 2.69966 3.4414C2.54832 3.48351 2.42556 3.59429 2.36821 3.74054C2.30216 3.90893 2.3929 4.18231 2.57437 4.72906L4.91642 11.7853C4.94759 11.8792 4.96317 11.9262 4.96933 11.9742C4.97479 12.0168 4.97473 12.0599 4.96916 12.1025C4.96289 12.1506 4.94718 12.1975 4.91577 12.2913Z" stroke="#737373" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 878 B |
@ -3,9 +3,8 @@ use crate::route::RouteServices;
|
||||
use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper};
|
||||
use crate::widgets;
|
||||
use crate::widgets::NostrWidget;
|
||||
use egui::{Response, Ui, Widget};
|
||||
use log::{error, info};
|
||||
use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction};
|
||||
use egui::{Response, ScrollArea, Ui, Widget};
|
||||
use nostrdb::{Filter, Note, NoteKey, Transaction};
|
||||
|
||||
pub struct HomePage {
|
||||
sub: SubWrapper,
|
||||
@ -40,7 +39,9 @@ impl NostrWidget for HomePage {
|
||||
.map_while(|f| f.map_or(None, |f| Some(f)))
|
||||
.collect();
|
||||
|
||||
info!("HomePage events: {}", events.len());
|
||||
ScrollArea::vertical()
|
||||
.show(ui, |ui| {
|
||||
widgets::StreamList::new(&events, &services).ui(ui)
|
||||
}).inner
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ use crate::note_util::OwnedNote;
|
||||
use crate::route;
|
||||
use crate::route::home::HomePage;
|
||||
use crate::route::stream::StreamPage;
|
||||
use crate::services::image_cache::ImageCache;
|
||||
use crate::services::ndb_wrapper::NDBWrapper;
|
||||
use crate::widgets::{Header, NostrWidget, StreamList};
|
||||
use egui::{Context, Response, ScrollArea, Ui, Widget};
|
||||
@ -47,6 +48,7 @@ pub struct Router {
|
||||
ndb: NDBWrapper,
|
||||
login: Option<[u8; 32]>,
|
||||
client: Client,
|
||||
image_cache: ImageCache,
|
||||
}
|
||||
|
||||
impl Router {
|
||||
@ -59,6 +61,7 @@ impl Router {
|
||||
ndb: NDBWrapper::new(ctx.clone(), ndb.clone(), client.clone()),
|
||||
client,
|
||||
login: None,
|
||||
image_cache: ImageCache::new(ctx.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,19 +106,18 @@ impl Router {
|
||||
ndb: &self.ndb,
|
||||
tx: &tx,
|
||||
login: &self.login,
|
||||
img_cache: &self.image_cache,
|
||||
};
|
||||
|
||||
// display app
|
||||
ScrollArea::vertical()
|
||||
.show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
Header::new().render(ui, &svc);
|
||||
if let Some(w) = self.current_widget.as_mut() {
|
||||
w.render(ui, &svc)
|
||||
} else {
|
||||
ui.label("No widget")
|
||||
}
|
||||
})
|
||||
.inner
|
||||
}).response
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,6 +128,7 @@ pub struct RouteServices<'a> {
|
||||
pub ndb: &'a NDBWrapper, //ref
|
||||
pub tx: &'a Transaction, //ref
|
||||
pub login: &'a Option<[u8; 32]>, //ref
|
||||
pub img_cache: &'a ImageCache,
|
||||
}
|
||||
|
||||
impl<'a> RouteServices<'a> {
|
||||
|
@ -1,11 +1,11 @@
|
||||
use crate::link::NostrLink;
|
||||
use crate::note_util::{NoteUtil, OwnedNote};
|
||||
use crate::note_util::OwnedNote;
|
||||
use crate::route::RouteServices;
|
||||
use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper};
|
||||
use crate::stream_info::StreamInfo;
|
||||
use crate::widgets::{Chat, NostrWidget, StreamPlayer};
|
||||
use egui::{Color32, Label, Response, RichText, TextWrapMode, Ui, Widget};
|
||||
use nostrdb::{Filter, NoteKey, Subscription, Transaction};
|
||||
use crate::widgets::{Chat, NostrWidget, StreamPlayer, StreamTitle, WriteChat};
|
||||
use egui::{Response, Ui, Vec2, Widget};
|
||||
use nostrdb::{Filter, NoteKey, Transaction};
|
||||
use std::borrow::Borrow;
|
||||
|
||||
pub struct StreamPage {
|
||||
@ -14,6 +14,7 @@ pub struct StreamPage {
|
||||
player: Option<StreamPlayer>,
|
||||
chat: Option<Chat>,
|
||||
sub: SubWrapper,
|
||||
new_msg: WriteChat,
|
||||
}
|
||||
|
||||
impl StreamPage {
|
||||
@ -29,6 +30,7 @@ impl StreamPage {
|
||||
.map_or(None, |n| Some(OwnedNote(n.note_key.as_u64()))),
|
||||
chat: None,
|
||||
player: None,
|
||||
new_msg: WriteChat::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -51,7 +53,7 @@ impl NostrWidget for StreamPage {
|
||||
if let Some(event) = event {
|
||||
if let Some(stream) = event.stream() {
|
||||
if self.player.is_none() {
|
||||
let p = StreamPlayer::new(ui.ctx(), &stream);
|
||||
let p = StreamPlayer::new(ui.ctx(), &stream.to_string());
|
||||
self.player = Some(p);
|
||||
}
|
||||
}
|
||||
@ -59,25 +61,27 @@ impl NostrWidget for StreamPage {
|
||||
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));
|
||||
StreamTitle::new(&event).render(ui, services);
|
||||
|
||||
if self.chat.is_none() {
|
||||
let chat = Chat::new(self.link.clone(), &services.ndb, services.tx);
|
||||
let ok = OwnedNote(event.key().unwrap().as_u64());
|
||||
let chat = Chat::new(self.link.clone(), ok, &services.ndb, services.tx);
|
||||
self.chat = Some(chat);
|
||||
}
|
||||
|
||||
let chat_h = 60.0;
|
||||
let w = ui.available_width();
|
||||
let h = ui.available_height();
|
||||
ui.allocate_ui(Vec2::new(w, h - chat_h), |ui| {
|
||||
if let Some(c) = self.chat.as_mut() {
|
||||
c.render(ui, services)
|
||||
} else {
|
||||
ui.label("Loading..")
|
||||
}
|
||||
});
|
||||
ui.allocate_ui(Vec2::new(w, chat_h), |ui| {
|
||||
self.new_msg.render(ui, services)
|
||||
}).response
|
||||
} else {
|
||||
ui.label("Loading..")
|
||||
}
|
||||
|
68
src/services/image_cache.rs
Normal file
68
src/services/image_cache.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use egui::Image;
|
||||
use log::{error, info};
|
||||
use nostr_sdk::util::hex;
|
||||
use sha2::digest::Update;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::hash::Hash;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub struct ImageCache {
|
||||
ctx: egui::Context,
|
||||
dir: PathBuf,
|
||||
fetch_lock: Arc<Mutex<HashSet<String>>>,
|
||||
}
|
||||
|
||||
impl ImageCache {
|
||||
pub fn new(ctx: egui::Context) -> Self {
|
||||
let out = PathBuf::from("./cache/images");
|
||||
fs::create_dir_all(&out).unwrap();
|
||||
Self {
|
||||
ctx,
|
||||
dir: out,
|
||||
fetch_lock: Arc::new(Mutex::new(HashSet::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find<U>(&self, url: U) -> PathBuf
|
||||
where
|
||||
U: Into<String>,
|
||||
{
|
||||
let mut sha = Sha256::new();
|
||||
sha2::digest::Update::update(&mut sha, url.into().as_bytes());
|
||||
let hash = hex::encode(sha.finalize());
|
||||
self.dir
|
||||
.join(PathBuf::from(hash[0..2].to_string()))
|
||||
.join(PathBuf::from(hash))
|
||||
}
|
||||
|
||||
pub fn load<'a, U>(&self, url: U) -> Image<'a>
|
||||
where
|
||||
U: Into<String>,
|
||||
{
|
||||
let u = url.into();
|
||||
let path = self.find(&u);
|
||||
if !path.exists() {
|
||||
let path = path.clone();
|
||||
let fl = self.fetch_lock.clone();
|
||||
let ctx = self.ctx.clone();
|
||||
tokio::spawn(async move {
|
||||
if fl.lock().await.insert(u.clone()) {
|
||||
info!("Fetching image: {}", &u);
|
||||
if let Ok(data) = reqwest::get(&u)
|
||||
.await {
|
||||
tokio::fs::create_dir_all(path.parent().unwrap()).await.unwrap();
|
||||
if let Err(e) = tokio::fs::write(path, data.bytes().await.unwrap()).await {
|
||||
error!("Failed to write file: {}", e);
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Image::from_uri(format!("file://{}", path.to_str().unwrap()))
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
pub mod ndb_wrapper;
|
||||
pub mod query;
|
||||
pub mod image_cache;
|
@ -142,14 +142,7 @@ impl NDBWrapper {
|
||||
.get_profile_by_pubkey(tx, pubkey)
|
||||
.map_or(None, |p| p.record().profile());
|
||||
|
||||
let sub = if p.is_none() {
|
||||
Some(self.subscribe(
|
||||
"profile",
|
||||
&[Filter::new().kinds([0]).authors([pubkey]).build()],
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let sub = None;
|
||||
(p, sub)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ use anyhow::Error;
|
||||
use chrono::Utc;
|
||||
use log::{error, info};
|
||||
use nostr_sdk::prelude::StreamExt;
|
||||
use nostr_sdk::Kind::Metadata;
|
||||
use nostr_sdk::{Client, Filter, SubscriptionId};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::Arc;
|
||||
@ -56,12 +57,24 @@ impl Query {
|
||||
|
||||
/// Return next query batch
|
||||
pub fn next(&mut self) -> Option<QueryTrace> {
|
||||
let next: Vec<QueryFilter> = self.queue.drain().collect();
|
||||
let mut next: Vec<QueryFilter> = self.queue.drain().collect();
|
||||
if next.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
let now = Utc::now();
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
// force profile queries into single filter
|
||||
if next.iter().all(|f| if let Some(k) = &f.kinds {
|
||||
k.len() == 1 && k.first().unwrap().as_u16() == 0
|
||||
} else {
|
||||
false
|
||||
}) {
|
||||
next = vec![Filter::new()
|
||||
.kinds([Metadata])
|
||||
.authors(next.iter().flat_map(|f| f.authors.as_ref().unwrap().clone()))
|
||||
]
|
||||
}
|
||||
Some(QueryTrace {
|
||||
id,
|
||||
filters: next,
|
||||
@ -145,6 +158,10 @@ where
|
||||
where
|
||||
F: Into<Vec<QueryFilter>>,
|
||||
{
|
||||
self.queue_into_queries.send(QueueDefer {
|
||||
id: id.to_string(),
|
||||
filters: filters.into(),
|
||||
}).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,38 +1,61 @@
|
||||
use crate::note_util::NoteUtil;
|
||||
use nostrdb::Note;
|
||||
use nostrdb::{NdbStrVariant, Note};
|
||||
|
||||
pub trait StreamInfo {
|
||||
fn title(&self) -> Option<String>;
|
||||
fn title(&self) -> Option<&str>;
|
||||
|
||||
fn summary(&self) -> Option<String>;
|
||||
fn summary(&self) -> Option<&str>;
|
||||
|
||||
fn host(&self) -> [u8; 32];
|
||||
fn host(&self) -> &[u8; 32];
|
||||
|
||||
fn stream(&self) -> Option<String>;
|
||||
fn stream(&self) -> Option<&str>;
|
||||
|
||||
fn starts(&self) -> u64;
|
||||
}
|
||||
|
||||
impl<'a> StreamInfo for Note<'a> {
|
||||
fn title(&self) -> Option<String> {
|
||||
fn title(&self) -> Option<&str> {
|
||||
if let Some(s) = self.get_tag_value("title") {
|
||||
s.variant().str().map(ToString::to_string)
|
||||
s.variant().str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn summary(&self) -> Option<String> {
|
||||
todo!()
|
||||
fn summary(&self) -> Option<&str> {
|
||||
if let Some(s) = self.get_tag_value("summary") {
|
||||
s.variant().str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn host(&self) -> [u8; 32] {
|
||||
todo!()
|
||||
fn host(&self) -> &[u8; 32] {
|
||||
match self.find_tag_value(|t| {
|
||||
t[0].variant().str() == Some("p") && t[3].variant().str() == Some("host")
|
||||
}) {
|
||||
Some(t) => match t.variant() {
|
||||
NdbStrVariant::Id(i) => i,
|
||||
NdbStrVariant::Str(s) => self.pubkey(),
|
||||
},
|
||||
None => self.pubkey(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stream(&self) -> Option<String> {
|
||||
fn stream(&self) -> Option<&str> {
|
||||
if let Some(s) = self.get_tag_value("streaming") {
|
||||
s.variant().str().map(ToString::to_string)
|
||||
s.variant().str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn starts(&self) -> u64 {
|
||||
if let Some(s) = self.get_tag_value("starts") {
|
||||
s.variant().str()
|
||||
.map_or(self.created_at(), |v| v.parse::<u64>().unwrap_or(self.created_at()))
|
||||
} else {
|
||||
self.created_at()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5
src/theme.rs
Normal file
5
src/theme.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use egui::Color32;
|
||||
|
||||
pub const PRIMARY: Color32 = Color32::from_rgb(248, 56, 217);
|
||||
pub const NEUTRAL_500: Color32 = Color32::from_rgb(115, 115, 115);
|
||||
pub const NEUTRAL_900: Color32 = Color32::from_rgb(23, 23, 23);
|
@ -1,10 +1,11 @@
|
||||
use crate::route::RouteServices;
|
||||
use crate::services::ndb_wrapper::SubWrapper;
|
||||
use egui::{Color32, Image, Rect, Response, Rounding, Sense, Ui, Vec2, Widget};
|
||||
use egui::{Color32, Image, Pos2, Response, Rounding, Sense, Ui, Vec2, Widget};
|
||||
|
||||
pub struct Avatar<'a> {
|
||||
image: Option<Image<'a>>,
|
||||
sub: Option<SubWrapper>,
|
||||
size: Option<f32>,
|
||||
}
|
||||
|
||||
impl<'a> Avatar<'a> {
|
||||
@ -12,6 +13,7 @@ impl<'a> Avatar<'a> {
|
||||
Self {
|
||||
image: Some(img),
|
||||
sub: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +21,7 @@ impl<'a> Avatar<'a> {
|
||||
Self {
|
||||
image: img,
|
||||
sub: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,39 +30,27 @@ impl<'a> Avatar<'a> {
|
||||
Self {
|
||||
image: img
|
||||
.map_or(None, |p| p.picture())
|
||||
.map(|p| Image::from_uri(p)),
|
||||
.map(|p| svc.img_cache.load(p)),
|
||||
sub,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_size(mut self, size: f32) -> Self {
|
||||
self.image = if let Some(i) = self.image {
|
||||
Some(i.max_height(size))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: f32) -> Self {
|
||||
self.image = if let Some(i) = self.image {
|
||||
Some(i.fit_to_exact_size(Vec2::new(size, size)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.size = Some(size);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Avatar<'a> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let size_v = self.size.unwrap_or(40.);
|
||||
let size = Vec2::new(size_v, size_v);
|
||||
match self.image {
|
||||
Some(img) => img.rounding(Rounding::same(ui.available_height())).ui(ui),
|
||||
Some(img) => img.fit_to_exact_size(size).rounding(Rounding::same(size_v)).ui(ui),
|
||||
None => {
|
||||
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));
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::click());
|
||||
painter.circle_filled(Pos2::new(size_v / 2., size_v / 2.), size_v / 2., Color32::from_rgb(200, 200, 200));
|
||||
response
|
||||
}
|
||||
}
|
||||
|
@ -2,19 +2,22 @@ use crate::link::NostrLink;
|
||||
use crate::note_util::OwnedNote;
|
||||
use crate::route::RouteServices;
|
||||
use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper};
|
||||
use crate::stream_info::StreamInfo;
|
||||
use crate::widgets::chat_message::ChatMessage;
|
||||
use crate::widgets::NostrWidget;
|
||||
use egui::{Response, ScrollArea, Ui, Widget};
|
||||
use egui::{Frame, Margin, Response, ScrollArea, Ui, Widget};
|
||||
use itertools::Itertools;
|
||||
use nostrdb::{Filter, Note, NoteKey, Transaction};
|
||||
|
||||
pub struct Chat {
|
||||
link: NostrLink,
|
||||
stream: OwnedNote,
|
||||
events: Vec<OwnedNote>,
|
||||
sub: SubWrapper,
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
pub fn new(link: NostrLink, ndb: &NDBWrapper, tx: &Transaction) -> Self {
|
||||
pub fn new(link: NostrLink, stream: OwnedNote, ndb: &NDBWrapper, tx: &Transaction) -> Self {
|
||||
let filter = Filter::new()
|
||||
.kinds([1_311])
|
||||
.tags([link.to_tag_value()], 'a')
|
||||
@ -26,6 +29,7 @@ impl Chat {
|
||||
Self {
|
||||
link,
|
||||
sub,
|
||||
stream,
|
||||
events: events
|
||||
.iter()
|
||||
.map(|n| OwnedNote(n.note_key.as_u64()))
|
||||
@ -51,15 +55,25 @@ impl NostrWidget for Chat {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let stream = services.ndb
|
||||
.get_note_by_key(services.tx, NoteKey::new(self.stream.0))
|
||||
.unwrap();
|
||||
|
||||
ScrollArea::vertical()
|
||||
.stick_to_bottom(true)
|
||||
.show(ui, |ui| {
|
||||
Frame::none()
|
||||
.outer_margin(Margin::symmetric(12., 8.))
|
||||
.show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
for ev in events {
|
||||
ChatMessage::new(&ev, services).ui(ui);
|
||||
ui.spacing_mut().item_spacing.y = 8.0;
|
||||
for ev in events.iter().sorted_by(|a, b| {
|
||||
a.starts().cmp(&b.starts())
|
||||
}) {
|
||||
ChatMessage::new(&stream, &ev, services).ui(ui);
|
||||
}
|
||||
})
|
||||
.response
|
||||
})
|
||||
.inner
|
||||
}).response
|
||||
}).inner
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,58 @@
|
||||
use crate::route::RouteServices;
|
||||
use crate::widgets::Profile;
|
||||
use eframe::epaint::Vec2;
|
||||
use egui::{Response, Ui, Widget};
|
||||
use nostrdb::Note;
|
||||
use crate::services::ndb_wrapper::SubWrapper;
|
||||
use crate::stream_info::StreamInfo;
|
||||
use crate::theme::{NEUTRAL_500, PRIMARY};
|
||||
use crate::widgets::Avatar;
|
||||
use eframe::epaint::text::TextWrapMode;
|
||||
use egui::text::LayoutJob;
|
||||
use egui::{Align, Color32, Label, Response, TextFormat, Ui, Widget};
|
||||
use nostrdb::{NdbProfile, Note};
|
||||
|
||||
pub struct ChatMessage<'a> {
|
||||
stream: &'a Note<'a>,
|
||||
ev: &'a Note<'a>,
|
||||
services: &'a RouteServices<'a>,
|
||||
profile: (Option<NdbProfile<'a>>, Option<SubWrapper>),
|
||||
}
|
||||
|
||||
impl<'a> ChatMessage<'a> {
|
||||
pub fn new(ev: &'a Note<'a>, services: &'a RouteServices<'a>) -> ChatMessage<'a> {
|
||||
ChatMessage { ev, services }
|
||||
pub fn new(stream: &'a Note<'a>, ev: &'a Note<'a>, services: &'a RouteServices<'a>) -> ChatMessage<'a> {
|
||||
ChatMessage { stream, ev, services, profile: services.ndb.fetch_profile(services.tx, ev.pubkey()) }
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
|
||||
let content = self.ev.content();
|
||||
ui.label(content);
|
||||
})
|
||||
.response
|
||||
let is_host = self.stream.host().eq(self.ev.pubkey());
|
||||
let name = self
|
||||
.profile.0
|
||||
.map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f));
|
||||
let img = self
|
||||
.profile.0
|
||||
.map_or(None, |f| f.picture().map(|f| self.services.img_cache.load(f)));
|
||||
|
||||
let name_color = if is_host {
|
||||
PRIMARY
|
||||
} else {
|
||||
NEUTRAL_500
|
||||
};
|
||||
|
||||
let mut format = TextFormat::default();
|
||||
format.line_height = Some(24.0);
|
||||
format.valign = Align::Center;
|
||||
|
||||
format.color = name_color;
|
||||
job.append(name, 0.0, format.clone());
|
||||
format.color = Color32::WHITE;
|
||||
job.append(self.ev.content(), 5.0, format.clone());
|
||||
|
||||
ui.add(Avatar::new_optional(img).size(24.));
|
||||
ui.add(Label::new(job)
|
||||
.wrap_mode(TextWrapMode::Wrap)
|
||||
);
|
||||
}).response
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ impl Header {
|
||||
|
||||
impl NostrWidget for Header {
|
||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
||||
let logo_bytes = include_bytes!("../logo.svg");
|
||||
let logo_bytes = include_bytes!("../resources/logo.svg");
|
||||
Frame::none()
|
||||
.outer_margin(Margin::symmetric(16., 8.))
|
||||
.show(ui, |ui| {
|
||||
|
@ -3,10 +3,12 @@ mod chat;
|
||||
mod chat_message;
|
||||
mod header;
|
||||
mod profile;
|
||||
mod stream;
|
||||
mod stream_tile;
|
||||
mod stream_list;
|
||||
mod stream_player;
|
||||
mod video_placeholder;
|
||||
mod stream_title;
|
||||
mod write_chat;
|
||||
|
||||
use crate::route::RouteServices;
|
||||
use egui::{Response, Ui};
|
||||
@ -22,3 +24,5 @@ pub use self::profile::Profile;
|
||||
pub use self::stream_list::StreamList;
|
||||
pub use self::stream_player::StreamPlayer;
|
||||
pub use self::video_placeholder::VideoPlaceholder;
|
||||
pub use self::stream_title::StreamTitle;
|
||||
pub use self::write_chat::WriteChat;
|
||||
|
@ -8,16 +8,21 @@ pub struct Profile<'a> {
|
||||
size: f32,
|
||||
pubkey: &'a [u8; 32],
|
||||
profile: Option<NdbProfile<'a>>,
|
||||
profile_image: Option<Image<'a>>,
|
||||
sub: Option<SubWrapper>,
|
||||
}
|
||||
|
||||
impl<'a> Profile<'a> {
|
||||
pub fn new(pubkey: &'a [u8; 32], services: &'a RouteServices<'a>) -> Self {
|
||||
let (p, sub) = services.ndb.fetch_profile(services.tx, pubkey);
|
||||
|
||||
let img = p
|
||||
.map_or(None, |f| f.picture().map(|f| services.img_cache.load(f)));
|
||||
Self {
|
||||
pubkey,
|
||||
size: 40.,
|
||||
profile: p,
|
||||
profile_image: img,
|
||||
sub,
|
||||
}
|
||||
}
|
||||
@ -32,17 +37,13 @@ impl<'a> Widget for Profile<'a> {
|
||||
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));
|
||||
ui.add(Avatar::new_optional(self.profile_image).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
|
||||
}).response
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::route::RouteServices;
|
||||
use crate::widgets::stream::StreamEvent;
|
||||
use crate::widgets::stream_tile::StreamEvent;
|
||||
use egui::{Frame, Margin, Response, Ui, Widget};
|
||||
use nostrdb::Note;
|
||||
|
||||
|
@ -6,6 +6,7 @@ use crate::widgets::VideoPlaceholder;
|
||||
use eframe::epaint::Vec2;
|
||||
use egui::{Color32, Image, Label, Response, RichText, Rounding, Sense, TextWrapMode, Ui, Widget};
|
||||
use nostrdb::{NdbStrVariant, Note};
|
||||
use crate::stream_info::StreamInfo;
|
||||
|
||||
pub struct StreamEvent<'a> {
|
||||
event: &'a Note<'a>,
|
||||
@ -18,7 +19,7 @@ impl<'a> StreamEvent<'a> {
|
||||
let image = event.get_tag_value("image");
|
||||
let cover = match image {
|
||||
Some(i) => match i.variant().str() {
|
||||
Some(i) => Some(Image::from_uri(i)),
|
||||
Some(i) => Some(services.img_cache.load(i)),
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
@ -35,15 +36,7 @@ impl Widget for StreamEvent<'_> {
|
||||
ui.vertical(|ui| {
|
||||
ui.style_mut().spacing.item_spacing = Vec2::new(12., 16.);
|
||||
|
||||
let host = match self.event.find_tag_value(|t| {
|
||||
t[0].variant().str() == Some("p") && t[3].variant().str() == Some("host")
|
||||
}) {
|
||||
Some(t) => match t.variant() {
|
||||
NdbStrVariant::Id(i) => i,
|
||||
NdbStrVariant::Str(s) => self.event.pubkey(),
|
||||
},
|
||||
None => self.event.pubkey(),
|
||||
};
|
||||
let host = self.event.host();
|
||||
let w = ui.available_width();
|
||||
let h = (w / 16.0) * 9.0;
|
||||
let img_size = Vec2::new(w, h);
|
||||
@ -64,10 +57,7 @@ impl Widget for StreamEvent<'_> {
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(Avatar::pubkey(&host, self.services).size(40.));
|
||||
let title = RichText::new(match self.event.get_tag_value("title") {
|
||||
Some(s) => s.variant().str().unwrap_or("Unknown"),
|
||||
None => "Unknown",
|
||||
})
|
||||
let title = RichText::new(self.event.title().unwrap_or("Untitled"))
|
||||
.size(16.)
|
||||
.color(Color32::WHITE);
|
||||
ui.add(Label::new(title).wrap_mode(TextWrapMode::Truncate));
|
42
src/widgets/stream_title.rs
Normal file
42
src/widgets/stream_title.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use crate::note_util::NoteUtil;
|
||||
use crate::route::RouteServices;
|
||||
use crate::stream_info::StreamInfo;
|
||||
use crate::widgets::{NostrWidget, Profile};
|
||||
use egui::{Color32, Frame, Label, Margin, Response, RichText, TextWrapMode, Ui, Widget};
|
||||
use nostrdb::Note;
|
||||
|
||||
pub struct StreamTitle<'a> {
|
||||
event: &'a Note<'a>,
|
||||
}
|
||||
|
||||
impl<'a> StreamTitle<'a> {
|
||||
pub fn new(event: &'a Note<'a>) -> StreamTitle {
|
||||
StreamTitle {
|
||||
event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> NostrWidget for StreamTitle<'a> {
|
||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
||||
Frame::none()
|
||||
.outer_margin(Margin::symmetric(12., 8.))
|
||||
.show(ui, |ui| {
|
||||
ui.style_mut().spacing.item_spacing.y = 8.;
|
||||
let title = RichText::new(self.event.title().unwrap_or("Untitled"))
|
||||
.size(20.)
|
||||
.color(Color32::WHITE);
|
||||
ui.add(Label::new(title.strong()).wrap_mode(TextWrapMode::Truncate));
|
||||
|
||||
Profile::new(self.event.host(), services)
|
||||
.size(32.)
|
||||
.ui(ui);
|
||||
|
||||
if let Some(summary) = self.event.get_tag_value("summary").map_or(None, |r| r.variant().str()) {
|
||||
let summary = RichText::new(summary)
|
||||
.color(Color32::WHITE);
|
||||
ui.add(Label::new(summary).wrap_mode(TextWrapMode::Truncate));
|
||||
}
|
||||
}).response
|
||||
}
|
||||
}
|
47
src/widgets/write_chat.rs
Normal file
47
src/widgets/write_chat.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use crate::route::RouteServices;
|
||||
use crate::theme::NEUTRAL_900;
|
||||
use crate::widgets::NostrWidget;
|
||||
use egui::{Button, Frame, Image, Margin, Rect, Response, Rounding, Sense, Shadow, Stroke, TextEdit, Ui, Vec2, Widget};
|
||||
use log::info;
|
||||
|
||||
pub struct WriteChat {
|
||||
msg: String,
|
||||
}
|
||||
|
||||
impl WriteChat {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
msg: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NostrWidget for WriteChat {
|
||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
||||
let size = ui.available_size();
|
||||
let logo_bytes = include_bytes!("../resources/send-03.svg");
|
||||
Frame::none()
|
||||
.inner_margin(Margin::symmetric(12., 6.))
|
||||
.stroke(Stroke::new(1.0, NEUTRAL_900))
|
||||
.show(ui, |ui| {
|
||||
Frame::none()
|
||||
.fill(NEUTRAL_900)
|
||||
.rounding(Rounding::same(12.0))
|
||||
.inner_margin(Margin::symmetric(12., 12.))
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let editor = TextEdit::singleline(&mut self.msg)
|
||||
.frame(false);
|
||||
ui.add(editor);
|
||||
if Image::from_bytes("send-03.svg", logo_bytes)
|
||||
.sense(Sense::click())
|
||||
.ui(ui)
|
||||
.clicked() {
|
||||
info!("Sending: {}", self.msg);
|
||||
self.msg.clear();
|
||||
}
|
||||
});
|
||||
})
|
||||
}).response
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user