commit
692638221a
16 changed files with 1906 additions and 212 deletions
820
Cargo.lock
generated
820
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
18
Cargo.toml
18
Cargo.toml
|
@ -5,18 +5,26 @@ authors = ["Jack Chakany <jack@chakany.systems>"]
|
|||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
profiling = ["dep:puffin", "dep:puffin_http", "eframe/puffin", "egui_extras/puffin"]
|
||||
|
||||
[dependencies]
|
||||
eframe = { version = "0.27.2", features = ["default", "puffin"] }
|
||||
eframe = { version = "0.27.2", features = ["default", "persistence"] }
|
||||
egui_extras = { version = "0.27.2", features = [
|
||||
"file",
|
||||
"image",
|
||||
"svg",
|
||||
"puffin",
|
||||
] }
|
||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "120971fc43db6ba0b6f194f4bd4a66f7e00a4e22" }
|
||||
image = { version = "0.25.1", features = ["jpeg", "png"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = "0.3.18"
|
||||
yandk = { path = "../yandk", features = ["coordinator"] }
|
||||
puffin = "0.19.0"
|
||||
puffin_http = "0.16.0"
|
||||
puffin = { version = "0.19.0", optional = true }
|
||||
puffin_http = { version = "0.16.0", optional = true }
|
||||
ewebsock = { version = "0.6.0", features = ["tls"] }
|
||||
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "ee8afeeb0b6695fca6d27dd0b74a8dc159e37b95" }
|
||||
rand = "0.8.5"
|
||||
nostr = { version = "0.32.1", features = ["std"] }
|
||||
serde = "1.0.204"
|
||||
serde_json = "1.0.121"
|
||||
|
|
2
Makefile
Normal file
2
Makefile
Normal file
|
@ -0,0 +1,2 @@
|
|||
dev:
|
||||
RUST_BACKTRACE=1 cargo run --features profiling
|
63
src/account_manager.rs
Normal file
63
src/account_manager.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use crate::keystorage::{Error, KeyStorage, KeyStorageType};
|
||||
use nostr::Keys;
|
||||
|
||||
pub struct AccountManager {
|
||||
pub loaded_keys: Vec<Keys>,
|
||||
}
|
||||
|
||||
impl AccountManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
loaded_keys: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_keys(&mut self) -> Result<Keys, Error> {
|
||||
let new_keypair = Keys::generate();
|
||||
self.loaded_keys.push(new_keypair.clone());
|
||||
|
||||
Ok(new_keypair)
|
||||
}
|
||||
|
||||
pub fn load_keys(&mut self) -> Result<Vec<Keys>, Error> {
|
||||
let mut keys = self.get_keys()?;
|
||||
keys.extend(self.loaded_keys.drain(..));
|
||||
keys.dedup();
|
||||
self.loaded_keys = keys.clone();
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
pub fn delete_key(&mut self, key: &Keys) -> Result<(), Error> {
|
||||
self.remove_key(key)?;
|
||||
if let Some(index) = self.loaded_keys.iter().position(|k| k == key) {
|
||||
self.loaded_keys.remove(index);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_platform_keystorage() -> KeyStorageType {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
return KeyStorageType::Linux;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
KeyStorageType::None
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStorage for AccountManager {
|
||||
fn get_keys(&self) -> Result<Vec<Keys>, Error> {
|
||||
Self::get_platform_keystorage().get_keys()
|
||||
}
|
||||
|
||||
fn add_key(&self, key: &Keys) -> Result<(), Error> {
|
||||
Self::get_platform_keystorage().add_key(key)
|
||||
}
|
||||
|
||||
fn remove_key(&self, key: &Keys) -> Result<(), Error> {
|
||||
Self::get_platform_keystorage().remove_key(key)
|
||||
}
|
||||
}
|
6
src/error.rs
Normal file
6
src/error.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
RelayNotConnected,
|
||||
}
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
118
src/keystorage/linux.rs
Normal file
118
src/keystorage/linux.rs
Normal file
|
@ -0,0 +1,118 @@
|
|||
use super::{Error, KeyStorage};
|
||||
use nostr::Keys;
|
||||
|
||||
pub struct LinuxKeyStorage {}
|
||||
|
||||
impl LinuxKeyStorage {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStorage for LinuxKeyStorage {
|
||||
fn get_keys(&self) -> Result<Vec<Keys>, Error> {
|
||||
let bfs = BasicFileStorage::new().get_keys()?;
|
||||
Ok(bfs)
|
||||
}
|
||||
fn add_key(&self, key: &Keys) -> Result<(), Error> {
|
||||
BasicFileStorage::new().add_key(key)?;
|
||||
Ok(())
|
||||
}
|
||||
fn remove_key(&self, key: &Keys) -> Result<(), Error> {
|
||||
BasicFileStorage::new().remove_key(key)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct BasicFileStorage {
|
||||
credentials_dir: String,
|
||||
}
|
||||
|
||||
impl BasicFileStorage {
|
||||
pub fn new() -> Self {
|
||||
BasicFileStorage {
|
||||
credentials_dir: ".credentials".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_private_key(&self, keypair: &Keys) -> Result<(), Error> {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
|
||||
fs::create_dir_all(&self.credentials_dir)?;
|
||||
|
||||
let public_key = keypair.public_key().to_hex();
|
||||
let private_key = keypair.secret_key().unwrap().to_secret_hex();
|
||||
|
||||
let file_path = Path::new(&self.credentials_dir).join(&public_key);
|
||||
let mut file = File::create(&file_path)?;
|
||||
file.write_all(private_key.as_bytes())?;
|
||||
|
||||
let mut perms = file.metadata()?.permissions();
|
||||
perms.set_mode(0o600);
|
||||
fs::set_permissions(&file_path, perms)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_private_keys(&self) -> Result<Vec<Keys>, Error> {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
let mut keypairs: Vec<Keys> = Vec::new();
|
||||
let dir_path = Path::new(&self.credentials_dir);
|
||||
|
||||
for entry in fs::read_dir(dir_path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file() {
|
||||
let mut file = File::open(&path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
if let Ok(keypair) = Keys::parse(contents) {
|
||||
keypairs.push(keypair);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(keypairs)
|
||||
}
|
||||
|
||||
fn remove_keypair(&self, keypair: &Keys) -> Result<(), Error> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let public_key = keypair.public_key().to_string();
|
||||
let file_path = Path::new(&self.credentials_dir).join(&public_key);
|
||||
|
||||
if file_path.exists() {
|
||||
fs::remove_file(file_path)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::IOError(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"Key file not found",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStorage for BasicFileStorage {
|
||||
fn get_keys(&self) -> Result<Vec<Keys>, Error> {
|
||||
let mmt = self.read_private_keys()?;
|
||||
Ok(mmt)
|
||||
}
|
||||
fn add_key(&self, key: &Keys) -> Result<(), Error> {
|
||||
self.write_private_key(key)?;
|
||||
Ok(())
|
||||
}
|
||||
fn remove_key(&self, key: &Keys) -> Result<(), Error> {
|
||||
self.remove_keypair(key)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
68
src/keystorage/mod.rs
Normal file
68
src/keystorage/mod.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use nostr::Keys;
|
||||
|
||||
mod linux;
|
||||
use linux::LinuxKeyStorage;
|
||||
|
||||
pub enum Error {
|
||||
IOError(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Error::IOError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::IOError(err) => write!(f, "IOError: {:?}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::IOError(err) => write!(f, "IO error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum KeyStorageType {
|
||||
None,
|
||||
#[cfg(target_os = "linux")]
|
||||
Linux,
|
||||
}
|
||||
|
||||
pub trait KeyStorage {
|
||||
fn get_keys(&self) -> Result<Vec<Keys>, Error>;
|
||||
fn add_key(&self, key: &Keys) -> Result<(), Error>;
|
||||
fn remove_key(&self, key: &Keys) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
impl KeyStorage for KeyStorageType {
|
||||
fn add_key(&self, key: &Keys) -> Result<(), Error> {
|
||||
match self {
|
||||
Self::None => Ok(()),
|
||||
#[cfg(target_os = "linux")]
|
||||
Self::Linux => LinuxKeyStorage::new().add_key(key),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_keys(&self) -> Result<Vec<Keys>, Error> {
|
||||
match self {
|
||||
Self::None => Ok(Vec::new()),
|
||||
#[cfg(target_os = "linux")]
|
||||
Self::Linux => LinuxKeyStorage::new().get_keys(),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_key(&self, key: &Keys) -> Result<(), Error> {
|
||||
match self {
|
||||
Self::None => Ok(()),
|
||||
#[cfg(target_os = "linux")]
|
||||
Self::Linux => LinuxKeyStorage::new().remove_key(key),
|
||||
}
|
||||
}
|
||||
}
|
353
src/main.rs
353
src/main.rs
|
@ -7,6 +7,12 @@ use egui::{Align, FontId, Layout};
|
|||
use egui_extras::{Column, TableBuilder};
|
||||
use tracing::{debug, error, info, Level};
|
||||
|
||||
mod account_manager;
|
||||
mod error;
|
||||
mod keystorage;
|
||||
mod relay;
|
||||
mod ui;
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout()); // add log files in prod one day
|
||||
tracing_subscriber::fmt()
|
||||
|
@ -14,6 +20,7 @@ fn main() -> Result<(), eframe::Error> {
|
|||
.with_max_level(Level::DEBUG)
|
||||
.init();
|
||||
|
||||
#[cfg(feature = "profiling")]
|
||||
start_puffin_server();
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
|
@ -40,26 +47,40 @@ fn main() -> Result<(), eframe::Error> {
|
|||
let _ = &cc
|
||||
.egui_ctx
|
||||
.style_mut(|style| style.visuals.dark_mode = false);
|
||||
Box::<Hoot>::default()
|
||||
Box::new(Hoot::new(cc))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Page {
|
||||
pub enum Page {
|
||||
Inbox,
|
||||
Drafts,
|
||||
Starred,
|
||||
Archived,
|
||||
Trash,
|
||||
Post,
|
||||
Settings,
|
||||
// TODO: fix this mess
|
||||
Onboarding,
|
||||
OnboardingNew,
|
||||
OnboardingNewShowKey,
|
||||
OnboardingReturning,
|
||||
}
|
||||
|
||||
struct Hoot {
|
||||
current_page: Page,
|
||||
// for storing the state of different components and such.
|
||||
#[derive(Default)]
|
||||
pub struct HootState {
|
||||
pub onboarding: ui::onboarding::OnboardingState,
|
||||
pub settings: ui::settings::SettingsState,
|
||||
}
|
||||
|
||||
pub struct Hoot {
|
||||
pub page: Page,
|
||||
focused_post: String,
|
||||
status: HootStatus,
|
||||
nostr: yandk::coordinator::Coordinator,
|
||||
state: HootState,
|
||||
relays: relay::RelayPool,
|
||||
ndb: nostrdb::Ndb,
|
||||
events: Vec<nostr::Event>,
|
||||
pub windows: Vec<Box<ui::compose_window::ComposeWindow>>,
|
||||
account_manager: account_manager::AccountManager,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
@ -68,100 +89,128 @@ enum HootStatus {
|
|||
Ready,
|
||||
}
|
||||
|
||||
impl Default for Hoot {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
fn update_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_function!();
|
||||
|
||||
impl Hoot {
|
||||
fn new() -> Self {
|
||||
let coordinator = yandk::coordinator::Coordinator::new();
|
||||
|
||||
Self {
|
||||
nostr: coordinator,
|
||||
current_page: Page::Inbox,
|
||||
focused_post: "".into(),
|
||||
status: HootStatus::Initalizing,
|
||||
if app.status == HootStatus::Initalizing {
|
||||
info!("Initalizing Hoot...");
|
||||
let ctx = ctx.clone();
|
||||
let wake_up = move || {
|
||||
ctx.request_repaint();
|
||||
};
|
||||
match app.account_manager.load_keys() {
|
||||
Ok(..) => {}
|
||||
Err(v) => error!("something went wrong trying to load keys: {}", v),
|
||||
}
|
||||
app.relays
|
||||
.add_url("wss://relay.damus.io".to_string(), wake_up.clone());
|
||||
app.relays
|
||||
.add_url("wss://relay-dev.hoot.sh".to_string(), wake_up);
|
||||
app.status = HootStatus::Ready;
|
||||
info!("Hoot Ready");
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Hoot {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
match self.status {
|
||||
HootStatus::Initalizing => {
|
||||
info!("Initalizing Hoot...");
|
||||
self.status = HootStatus::Ready;
|
||||
let cloned_ctx = ctx.clone();
|
||||
let refresh_func = move || {
|
||||
cloned_ctx.request_repaint();
|
||||
};
|
||||
let _ = self.nostr.add_relay("".to_string(), refresh_func);
|
||||
let new_val = app.relays.try_recv();
|
||||
if new_val.is_some() {
|
||||
info!("{:?}", new_val.clone());
|
||||
|
||||
use relay::RelayMessage;
|
||||
let deserialized: RelayMessage =
|
||||
serde_json::from_str(new_val.unwrap().as_str()).expect("relay sent us bad json");
|
||||
|
||||
use RelayMessage::*;
|
||||
match deserialized {
|
||||
Event {
|
||||
subscription_id,
|
||||
event,
|
||||
} => {
|
||||
app.events.push(event);
|
||||
}
|
||||
_ => {
|
||||
// who cares rn
|
||||
}
|
||||
HootStatus::Ready => self.nostr.try_recv(), // we want to recieve events now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
egui::SidePanel::left("sidebar").show(ctx, |ui| {
|
||||
ui.heading("Hoot");
|
||||
ui.vertical(|ui| {
|
||||
ui.style_mut()
|
||||
.text_styles
|
||||
.insert(Button, FontId::new(20.0, Proportional));
|
||||
ui.selectable_value(&mut self.current_page, Page::Inbox, "Inbox");
|
||||
ui.selectable_value(&mut self.current_page, Page::Drafts, "Drafts");
|
||||
ui.selectable_value(&mut self.current_page, Page::Starred, "Starred");
|
||||
ui.selectable_value(&mut self.current_page, Page::Archived, "Archived");
|
||||
ui.selectable_value(&mut self.current_page, Page::Trash, "Trash");
|
||||
fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_function!();
|
||||
|
||||
ui.with_layout(Layout::bottom_up(Align::LEFT), |ui| {
|
||||
let my_key = yandk::Pubkey::from_hex(
|
||||
"c5fb6ecc876e0458e3eca9918e370cbcd376901c58460512fe537a46e58c38bb",
|
||||
)
|
||||
.unwrap();
|
||||
let maybe_profile = match self.nostr.get_profile(my_key.bytes()) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
error!("error when getting profile: {}", e);
|
||||
ui.label("Loading...");
|
||||
ui.label("Loading...");
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(p) = maybe_profile {
|
||||
let record = p.record();
|
||||
if let Some(profile) = record.profile() {
|
||||
if let Some(nip_05) = profile.nip05() {
|
||||
ui.label(nip_05);
|
||||
} else {
|
||||
ui.label("No Nostr Address");
|
||||
}
|
||||
if let Some(display_name) = profile.display_name() {
|
||||
ui.label(display_name);
|
||||
} else if let Some(name) = profile.name() {
|
||||
ui.label(format!("@{}", name));
|
||||
} else {
|
||||
let hex = my_key.hex();
|
||||
ui.label(hex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label("Loading...");
|
||||
ui.label("Loading...");
|
||||
}
|
||||
ui.separator();
|
||||
});
|
||||
});
|
||||
if app.page == Page::Onboarding
|
||||
|| app.page == Page::OnboardingNew
|
||||
|| app.page == Page::OnboardingNewShowKey
|
||||
|| app.page == Page::OnboardingReturning
|
||||
{
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui::onboarding::OnboardingScreen::ui(app, ui);
|
||||
});
|
||||
egui::TopBottomPanel::top("search").show(ctx, |ui| {
|
||||
} else {
|
||||
egui::SidePanel::left("Side Navbar").show(ctx, |ui| {
|
||||
ui.heading("Hoot");
|
||||
if ui.button("Inbox").clicked() {
|
||||
app.page = Page::Inbox;
|
||||
}
|
||||
if ui.button("Drafts").clicked() {
|
||||
app.page = Page::Drafts;
|
||||
}
|
||||
if ui.button("Settings").clicked() {
|
||||
app.page = Page::Settings;
|
||||
}
|
||||
if ui.button("Onboarding").clicked() {
|
||||
app.page = Page::Onboarding;
|
||||
}
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("Search").show(ctx, |ui| {
|
||||
ui.heading("Search");
|
||||
});
|
||||
egui::CentralPanel::default().show(ctx, |ui| match self.current_page {
|
||||
Page::Inbox => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
ui.heading("Inbox");
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
// todo: fix
|
||||
for window in &mut app.windows {
|
||||
window.show(ui);
|
||||
}
|
||||
|
||||
if app.page == Page::Inbox {
|
||||
ui.label("hello there!");
|
||||
if ui.button("Compose").clicked() {
|
||||
let mut new_window = Box::new(ui::compose_window::ComposeWindow::new());
|
||||
new_window.show(ui);
|
||||
app.windows.push(new_window);
|
||||
}
|
||||
|
||||
if ui.button("Send Test Event").clicked() {
|
||||
let temp_keys = nostr::Keys::generate();
|
||||
// todo: lmao
|
||||
let new_event = nostr::EventBuilder::text_note("GFY!", [])
|
||||
.to_event(&temp_keys)
|
||||
.unwrap();
|
||||
let event_json = crate::relay::ClientMessage::Event { event: new_event };
|
||||
let _ = &app
|
||||
.relays
|
||||
.send(ewebsock::WsMessage::Text(
|
||||
serde_json::to_string(&event_json).unwrap(),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if ui.button("Get kind 1 notes").clicked() {
|
||||
let mut filter = nostr::types::Filter::new();
|
||||
filter = filter.kind(nostr::Kind::TextNote);
|
||||
let mut sub = crate::relay::Subscription::default();
|
||||
sub.filter(filter);
|
||||
let c_msg = crate::relay::ClientMessage::from(sub);
|
||||
|
||||
let _ = &app
|
||||
.relays
|
||||
.send(ewebsock::WsMessage::Text(
|
||||
serde_json::to_string(&c_msg).unwrap(),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
TableBuilder::new(ui)
|
||||
.column(Column::auto())
|
||||
.column(Column::auto())
|
||||
|
@ -169,11 +218,47 @@ impl eframe::App for Hoot {
|
|||
.column(Column::remainder())
|
||||
.column(Column::remainder())
|
||||
.striped(true)
|
||||
.auto_shrink(Vec2b { x: false, y: false })
|
||||
.sense(Sense::click())
|
||||
.auto_shrink(Vec2b { x: false, y: false })
|
||||
.header(20.0, |_header| {})
|
||||
.body(|mut body| {
|
||||
puffin::profile_scope!("table rendering");
|
||||
for event in app.events.clone() {
|
||||
body.row(30.0, |mut row| {
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(event.pubkey.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(event.content.clone());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("2 minutes ago");
|
||||
});
|
||||
});
|
||||
}
|
||||
body.row(30.0, |mut row| {
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("Elon Musk");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("Second Test Message");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("2 minutes ago");
|
||||
});
|
||||
});
|
||||
|
||||
body.row(30.0, |mut row| {
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
|
@ -185,59 +270,51 @@ impl eframe::App for Hoot {
|
|||
ui.label("Jack Chakany");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("Hello! Just checking in...");
|
||||
ui.label("Message Content");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("5 min ago");
|
||||
ui.label("5 minutes ago");
|
||||
});
|
||||
|
||||
if row.response().clicked() {
|
||||
self.current_page = Page::Post;
|
||||
}
|
||||
});
|
||||
body.row(30.0, |mut row| {
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("Karnage");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("New designs!");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("10 min ago");
|
||||
});
|
||||
|
||||
if row.response().clicked() {
|
||||
self.current_page = Page::Post;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Page::Drafts => {
|
||||
ui.heading("Drafts");
|
||||
}
|
||||
Page::Starred => {
|
||||
ui.heading("Starred");
|
||||
}
|
||||
Page::Archived => {
|
||||
ui.heading("Archived");
|
||||
}
|
||||
Page::Trash => {
|
||||
ui.heading("Trash");
|
||||
}
|
||||
Page::Post => {
|
||||
// used for viewing messages duh
|
||||
ui.heading("Message");
|
||||
ui.label(format!("{}", self.focused_post));
|
||||
} else if app.page == Page::Settings {
|
||||
ui.heading("Settings");
|
||||
ui::settings::SettingsScreen::ui(app, ui);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Hoot {
|
||||
fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
let storage_dir = eframe::storage_dir("Hoot").unwrap();
|
||||
let mut ndb_config = nostrdb::Config::new();
|
||||
ndb_config.set_ingester_threads(3);
|
||||
|
||||
let ndb = nostrdb::Ndb::new(storage_dir.to_str().unwrap(), &ndb_config)
|
||||
.expect("could not load nostrdb");
|
||||
Self {
|
||||
page: Page::Inbox,
|
||||
focused_post: "".into(),
|
||||
status: HootStatus::Initalizing,
|
||||
state: Default::default(),
|
||||
relays: relay::RelayPool::new(),
|
||||
ndb,
|
||||
events: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
account_manager: account_manager::AccountManager::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Hoot {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
update_app(self, ctx);
|
||||
render_app(self, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "profiling")]
|
||||
fn start_puffin_server() {
|
||||
puffin::set_scopes_on(true); // tell puffin to collect data
|
||||
|
||||
|
|
195
src/relay/message.rs
Normal file
195
src/relay/message.rs
Normal file
|
@ -0,0 +1,195 @@
|
|||
use ewebsock::WsMessage;
|
||||
use nostr::types::Filter;
|
||||
use nostr::Event;
|
||||
use serde::de::{SeqAccess, Visitor};
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt::{self};
|
||||
|
||||
/// Messages that are client <- relay.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RelayMessage {
|
||||
Event {
|
||||
subscription_id: String,
|
||||
event: Event,
|
||||
},
|
||||
Ok {
|
||||
event_id: String,
|
||||
accepted: bool,
|
||||
message: String,
|
||||
},
|
||||
Eose {
|
||||
subscription_id: String,
|
||||
},
|
||||
Closed {
|
||||
subscription_id: String,
|
||||
message: String,
|
||||
},
|
||||
Notice {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<WsMessage> for RelayMessage {
|
||||
fn from(value: WsMessage) -> Self {
|
||||
match value {
|
||||
WsMessage::Text(text) => {
|
||||
let parsed: RelayMessage = match serde_json::from_str(&text) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
panic!("could not parse message: {}", e);
|
||||
}
|
||||
};
|
||||
return parsed;
|
||||
}
|
||||
_ => {
|
||||
panic!("Cannot parse anything but text into a RelayMessage");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RelayMessage {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct RelayMessageVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for RelayMessageVisitor {
|
||||
type Value = RelayMessage;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a sequence starting with 'EVENT' or 'OK'")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let tag: &str = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
|
||||
|
||||
match tag {
|
||||
"EVENT" => {
|
||||
let subscription_id: String = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
|
||||
let event: Event = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
|
||||
Ok(RelayMessage::Event {
|
||||
subscription_id,
|
||||
event,
|
||||
})
|
||||
}
|
||||
"OK" => {
|
||||
let event_id: String = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
|
||||
let accepted: bool = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
|
||||
let message: String = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
|
||||
Ok(RelayMessage::Ok {
|
||||
event_id,
|
||||
accepted,
|
||||
message,
|
||||
})
|
||||
}
|
||||
"EOSE" => {
|
||||
let subscription_id: String = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
|
||||
Ok(RelayMessage::Eose { subscription_id })
|
||||
}
|
||||
"CLOSED" => {
|
||||
let subscription_id: String = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
|
||||
let message: String = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
|
||||
Ok(RelayMessage::Closed {
|
||||
subscription_id,
|
||||
message,
|
||||
})
|
||||
}
|
||||
"NOTICE" => {
|
||||
let message: String = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
|
||||
Ok(RelayMessage::Notice { message })
|
||||
}
|
||||
_ => Err(serde::de::Error::invalid_value(
|
||||
serde::de::Unexpected::Str(&tag),
|
||||
&self,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_seq(RelayMessageVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// Messages that are client -> relay.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ClientMessage {
|
||||
Event {
|
||||
event: Event,
|
||||
},
|
||||
Req {
|
||||
subscription_id: String,
|
||||
filters: Vec<Filter>,
|
||||
},
|
||||
Close {
|
||||
subscription_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<super::Subscription> for ClientMessage {
|
||||
fn from(value: super::Subscription) -> Self {
|
||||
Self::Req {
|
||||
subscription_id: value.id,
|
||||
filters: value.filters,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ClientMessage {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
ClientMessage::Event { event } => {
|
||||
let mut seq = serializer.serialize_seq(Some(2))?;
|
||||
seq.serialize_element("EVENT")?;
|
||||
seq.serialize_element(event)?;
|
||||
seq.end()
|
||||
}
|
||||
ClientMessage::Req {
|
||||
subscription_id,
|
||||
filters,
|
||||
} => {
|
||||
let mut seq = serializer.serialize_seq(Some(2 + filters.len()))?;
|
||||
seq.serialize_element("REQ")?;
|
||||
seq.serialize_element(subscription_id)?;
|
||||
for filter in filters {
|
||||
seq.serialize_element(filter)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
ClientMessage::Close { subscription_id } => {
|
||||
let mut seq = serializer.serialize_seq(Some(2))?;
|
||||
seq.serialize_element("CLOSE")?;
|
||||
seq.serialize_element(subscription_id)?;
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
120
src/relay/mod.rs
Normal file
120
src/relay/mod.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
use ewebsock::{WsEvent, WsMessage};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
mod pool;
|
||||
pub use pool::RelayPool;
|
||||
|
||||
mod message;
|
||||
pub use message::{ClientMessage, RelayMessage};
|
||||
|
||||
mod subscription;
|
||||
pub use subscription::Subscription;
|
||||
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
pub enum RelayStatus {
|
||||
Connecting,
|
||||
Connected,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
pub struct Relay {
|
||||
pub url: String,
|
||||
reader: ewebsock::WsReceiver,
|
||||
writer: ewebsock::WsSender,
|
||||
pub status: RelayStatus,
|
||||
}
|
||||
|
||||
impl Relay {
|
||||
pub fn new_with_wakeup(
|
||||
url: impl Into<String>,
|
||||
wake_up: impl Fn() + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
let new_url: String = url.into();
|
||||
let (sender, reciever) =
|
||||
ewebsock::connect_with_wakeup(new_url.clone(), ewebsock::Options::default(), wake_up)
|
||||
.unwrap();
|
||||
|
||||
let mut relay = Self {
|
||||
url: new_url,
|
||||
reader: reciever,
|
||||
writer: sender,
|
||||
status: RelayStatus::Connecting,
|
||||
};
|
||||
|
||||
relay.ping();
|
||||
|
||||
relay
|
||||
}
|
||||
|
||||
pub fn send(&mut self, message: WsMessage) -> Result<()> {
|
||||
if self.status != RelayStatus::Connected {
|
||||
return Err(Error::RelayNotConnected);
|
||||
}
|
||||
|
||||
self.writer.send(message);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn try_recv(&mut self) -> Option<String> {
|
||||
if let Some(event) = self.reader.try_recv() {
|
||||
use WsEvent::*;
|
||||
match event {
|
||||
Message(message) => {
|
||||
return self.handle_message(message);
|
||||
}
|
||||
Opened => {
|
||||
self.status = RelayStatus::Connected;
|
||||
}
|
||||
Error(error) => {
|
||||
error!("error in websocket connection to {}: {}", self.url, error);
|
||||
}
|
||||
Closed => {
|
||||
info!("connection to {} closed", self.url);
|
||||
self.status = RelayStatus::Disconnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_message(&mut self, message: WsMessage) -> Option<String> {
|
||||
use WsMessage::*;
|
||||
match message {
|
||||
Text(txt) => {
|
||||
return Some(txt);
|
||||
}
|
||||
Binary(..) => {
|
||||
error!("recived binary messsage, your move semisol");
|
||||
}
|
||||
Ping(d) => {
|
||||
let pong_msg = WsMessage::Pong(d);
|
||||
match self.send(pong_msg) {
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("error when sending websocket message {:?}", e),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// who cares
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn ping(&mut self) {
|
||||
let ping_msg = WsMessage::Ping(Vec::new());
|
||||
match self.send(ping_msg) {
|
||||
Ok(_) => {
|
||||
info!("Ping sent to {}", self.url);
|
||||
self.status = RelayStatus::Connected;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error sending ping to {}: {:?}", self.url, e);
|
||||
self.status = RelayStatus::Disconnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
src/relay/pool.rs
Normal file
49
src/relay/pool.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use crate::error::Result;
|
||||
use crate::relay::{Relay, RelayStatus};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct RelayPool {
|
||||
pub relays: HashMap<String, Relay>,
|
||||
}
|
||||
|
||||
impl RelayPool {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
relays: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_url(&mut self, url: String, wake_up: impl Fn() + Send + Sync + 'static) {
|
||||
let relay = Relay::new_with_wakeup(url.clone(), wake_up);
|
||||
self.relays.insert(url, relay);
|
||||
}
|
||||
|
||||
pub fn remove_url(&mut self, url: &str) -> Option<Relay> {
|
||||
self.relays.remove(url)
|
||||
}
|
||||
|
||||
pub fn try_recv(&mut self) -> Option<String> {
|
||||
for relay in self.relays.values_mut() {
|
||||
if let Some(message) = relay.try_recv() {
|
||||
return Some(message);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn send(&mut self, message: ewebsock::WsMessage) -> Result<()> {
|
||||
for relay in self.relays.values_mut() {
|
||||
if relay.status == RelayStatus::Connected {
|
||||
relay.send(message.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ping_all(&mut self) -> Result<()> {
|
||||
for relay in self.relays.values_mut() {
|
||||
relay.ping();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
29
src/relay/subscription.rs
Normal file
29
src/relay/subscription.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use nostr::types::Filter;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Subscription {
|
||||
pub id: String,
|
||||
pub filters: Vec<Filter>,
|
||||
}
|
||||
|
||||
impl Default for Subscription {
|
||||
fn default() -> Self {
|
||||
let s: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
Self::new(s, vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
pub fn new(id: String, filters: Vec<Filter>) -> Self {
|
||||
Self { id, filters }
|
||||
}
|
||||
|
||||
pub fn filter(&mut self, filter: Filter) {
|
||||
self.filters.push(filter)
|
||||
}
|
||||
}
|
39
src/ui/compose_window.rs
Normal file
39
src/ui/compose_window.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use crate::Hoot;
|
||||
use eframe::egui::{self, RichText};
|
||||
use rand::random;
|
||||
|
||||
pub struct ComposeWindow {
|
||||
title: Option<RichText>,
|
||||
id: egui::Id,
|
||||
subject: String,
|
||||
to_field: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl ComposeWindow {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
title: None,
|
||||
id: egui::Id::new(random::<u32>()),
|
||||
subject: String::from("New Message"),
|
||||
to_field: String::new(),
|
||||
content: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ui: &mut egui::Ui) {
|
||||
egui::Window::new(&self.subject)
|
||||
.id(self.id)
|
||||
.show(ui.ctx(), |ui| {
|
||||
ui.label("Hello!");
|
||||
ui.vertical(|ui| {
|
||||
ui.text_edit_singleline(&mut self.to_field);
|
||||
ui.text_edit_singleline(&mut self.subject);
|
||||
ui.add_sized(
|
||||
ui.available_size(),
|
||||
egui::TextEdit::multiline(&mut self.content),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
9
src/ui/mod.rs
Normal file
9
src/ui/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use eframe::egui;
|
||||
|
||||
pub mod compose_window;
|
||||
pub mod onboarding;
|
||||
pub mod settings;
|
||||
|
||||
pub trait View {
|
||||
fn ui(&mut self, ui: &mut egui::Ui);
|
||||
}
|
95
src/ui/onboarding.rs
Normal file
95
src/ui/onboarding.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
use crate::{Hoot, Page};
|
||||
use eframe::egui;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct OnboardingState {
|
||||
// for nsecs, etc.
|
||||
pub secret_input: String,
|
||||
}
|
||||
|
||||
pub struct OnboardingScreen {}
|
||||
|
||||
impl OnboardingScreen {
|
||||
pub fn ui(app: &mut Hoot, ui: &mut egui::Ui) {
|
||||
ui.heading("Welcome to Hoot Mail!");
|
||||
|
||||
match app.page {
|
||||
Page::Onboarding => Self::onboarding_home(app, ui),
|
||||
Page::OnboardingNew => Self::onboarding_new(app, ui),
|
||||
Page::OnboardingNewShowKey => Self::onboarding_new_keypair_generated(app, ui),
|
||||
Page::OnboardingReturning => Self::onboarding_returning(app, ui),
|
||||
_ => error!("OnboardingScreen should not be displayed when page is not Onboarding!"),
|
||||
}
|
||||
}
|
||||
|
||||
fn onboarding_home(app: &mut Hoot, ui: &mut egui::Ui) {
|
||||
if ui.button("I am new to Hoot Mail").clicked() {
|
||||
app.page = Page::OnboardingNew;
|
||||
}
|
||||
|
||||
if ui.button("I have used Hoot Mail before.").clicked() {
|
||||
app.page = Page::OnboardingReturning;
|
||||
}
|
||||
}
|
||||
|
||||
fn onboarding_new(app: &mut Hoot, ui: &mut egui::Ui) {
|
||||
if ui.button("Go Back").clicked() {
|
||||
app.page = Page::Onboarding;
|
||||
}
|
||||
ui.label("To setup Hoot Mail, you need a nostr identity.");
|
||||
|
||||
if ui.button("Create new keypair").clicked() {
|
||||
let _ = app.account_manager.generate_keys();
|
||||
app.page = Page::OnboardingNewShowKey;
|
||||
}
|
||||
}
|
||||
|
||||
fn onboarding_new_keypair_generated(app: &mut Hoot, ui: &mut egui::Ui) {
|
||||
use crate::keystorage::KeyStorage;
|
||||
use nostr::ToBech32;
|
||||
|
||||
let first_key = app.account_manager.loaded_keys[0].clone();
|
||||
ui.label(format!(
|
||||
"New identity: {}",
|
||||
first_key.public_key().to_bech32().unwrap()
|
||||
));
|
||||
|
||||
if ui.button("OK, Save!").clicked() {
|
||||
app.account_manager
|
||||
.add_key(&first_key)
|
||||
.expect("could not write key");
|
||||
|
||||
app.page = Page::Inbox;
|
||||
}
|
||||
}
|
||||
|
||||
fn onboarding_returning(app: &mut Hoot, ui: &mut egui::Ui) {
|
||||
if ui.button("Go Back").clicked() {
|
||||
app.page = Page::Onboarding;
|
||||
}
|
||||
ui.label("Welcome Back!");
|
||||
|
||||
let parsed_secret_key = nostr::SecretKey::parse(&app.state.onboarding.secret_input);
|
||||
let valid_key = parsed_secret_key.is_ok();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Please enter your nsec here:");
|
||||
ui.text_edit_singleline(&mut app.state.onboarding.secret_input);
|
||||
match valid_key {
|
||||
true => ui.colored_label(egui::Color32::LIGHT_GREEN, "✔ Key Valid"),
|
||||
false => ui.colored_label(egui::Color32::RED, "⊗ Key Invalid"),
|
||||
}
|
||||
});
|
||||
|
||||
if ui
|
||||
.add_enabled(valid_key, egui::Button::new("Save"))
|
||||
.clicked()
|
||||
{
|
||||
use crate::keystorage::KeyStorage;
|
||||
let keypair = nostr::Keys::new(parsed_secret_key.unwrap());
|
||||
let _ = app.account_manager.add_key(&keypair);
|
||||
let _ = app.account_manager.load_keys();
|
||||
app.page = Page::Inbox;
|
||||
}
|
||||
}
|
||||
}
|
134
src/ui/settings.rs
Normal file
134
src/ui/settings.rs
Normal file
|
@ -0,0 +1,134 @@
|
|||
use crate::Hoot;
|
||||
use eframe::egui::{self, Color32, Direction, Layout, Sense, Ui, Vec2};
|
||||
use egui_tabs::Tabs;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SettingsState {
|
||||
pub new_relay_url: String,
|
||||
}
|
||||
|
||||
enum Tab {
|
||||
Profile = 0,
|
||||
Relays = 1,
|
||||
Identity = 2,
|
||||
}
|
||||
|
||||
impl From<i32> for Tab {
|
||||
fn from(value: i32) -> Self {
|
||||
match value {
|
||||
0 => Tab::Profile,
|
||||
1 => Tab::Relays,
|
||||
2 => Tab::Identity,
|
||||
_ => Tab::Profile, // Default to Profile for invalid values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tab> for i32 {
|
||||
fn from(tab: Tab) -> Self {
|
||||
tab as i32
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SettingsScreen {}
|
||||
|
||||
impl SettingsScreen {
|
||||
pub fn ui(app: &mut Hoot, ui: &mut Ui) {
|
||||
let tabs_response = Tabs::new(3)
|
||||
.height(16.0)
|
||||
.selected(0)
|
||||
.layout(Layout::centered_and_justified(Direction::TopDown))
|
||||
.show(ui, |ui, state| {
|
||||
let current_tab = Tab::from(state.index());
|
||||
use Tab::*;
|
||||
let tab_label = match current_tab {
|
||||
Profile => "My Profile",
|
||||
Relays => "Relays",
|
||||
Identity => "Keys",
|
||||
};
|
||||
ui.add(egui::Label::new(tab_label).selectable(false));
|
||||
});
|
||||
let current_tab: Tab = tabs_response.selected().unwrap().into();
|
||||
|
||||
use Tab::*;
|
||||
match current_tab {
|
||||
Profile => Self::profile(app, ui),
|
||||
Relays => Self::relays(app, ui),
|
||||
Identity => Self::identity(app, ui),
|
||||
}
|
||||
}
|
||||
|
||||
fn profile(app: &mut Hoot, ui: &mut Ui) {
|
||||
ui.label("Your profile.");
|
||||
}
|
||||
|
||||
fn relays(app: &mut Hoot, ui: &mut Ui) {
|
||||
ui.heading("Relays");
|
||||
ui.small("A relay is a server that Hoot connects with to send & receive messages.");
|
||||
|
||||
ui.label("Add New Relay:");
|
||||
ui.horizontal(|ui| {
|
||||
let new_relay = &mut app.state.settings.new_relay_url;
|
||||
ui.text_edit_singleline(new_relay);
|
||||
if ui.button("Add Relay").clicked() && !new_relay.is_empty() {
|
||||
let ctx = ui.ctx().clone();
|
||||
let wake_up = move || {
|
||||
ctx.request_repaint();
|
||||
};
|
||||
app.relays.add_url(new_relay.clone(), wake_up);
|
||||
app.state.settings.new_relay_url = String::new(); // clears field
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.label("Your Relays:");
|
||||
ui.vertical(|ui| {
|
||||
let mut relay_to_remove: Option<String> = None;
|
||||
for (url, relay) in app.relays.relays.iter() {
|
||||
ui.horizontal(|ui| {
|
||||
use crate::relay::RelayStatus::*;
|
||||
let conn_fill: Color32 = match relay.status {
|
||||
Connecting => Color32::YELLOW,
|
||||
Connected => Color32::LIGHT_GREEN,
|
||||
Disconnected => Color32::RED,
|
||||
};
|
||||
|
||||
let size = Vec2::splat(12.0);
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::hover());
|
||||
let rect = response.rect;
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0 - 1.0;
|
||||
painter.circle_filled(c, r, conn_fill);
|
||||
|
||||
ui.label(url);
|
||||
if ui.button("Remove Relay").clicked() {
|
||||
relay_to_remove = Some(url.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if relay_to_remove.is_some() {
|
||||
app.relays.remove_url(&relay_to_remove.unwrap());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn identity(app: &mut Hoot, ui: &mut Ui) {
|
||||
ui.vertical(|ui| {
|
||||
use nostr::ToBech32;
|
||||
for key in app.account_manager.loaded_keys.clone() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("Key ID: {}", key.public_key().to_bech32().unwrap()));
|
||||
if ui.button("Remove Key").clicked() {
|
||||
match app.account_manager.delete_key(&key) {
|
||||
Ok(..) => {}
|
||||
Err(v) => error!("couldn't remove key: {}", v),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue