Merge branch 'dilger/unstable' into feature/relay-list-widget

# Conflicts:
#	src/ui/mod.rs
This commit is contained in:
Bu5hm4nn 2023-08-17 12:06:40 -10:00
commit c3130f23f2
48 changed files with 3762 additions and 2161 deletions

728
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,8 @@ fallible-iterator = "0.2"
filetime = "0.2"
futures = "0.3"
futures-util = "0.3"
gossip-relay-picker = { git = "https://github.com/mikedilger/gossip-relay-picker", rev = "a1a81895641eb59c25792376b2efc7682b09c156" }
gossip-relay-picker = { git = "https://github.com/mikedilger/gossip-relay-picker", rev = "67436e26cb61596b1ff459693f2fe5999854efe2" }
heed = { git = "https://github.com/mikedilger/heed", rev = "b876c8fc6b256f081a3f3b94f06a08ffb44aa614" }
hex = "0.4"
http = "0.2"
humansize = "2.1"
@ -45,11 +46,9 @@ image = { version = "0.24.6", features = [ "png", "jpeg" ] }
kamadak-exif = "0.5"
lazy_static = "1.4"
linkify = "0.9"
lmdb-rkv = "0.14"
lmdb-rkv-sys = "0.11"
memoize = "0.4"
mime = "0.3"
nostr-types = { git = "https://github.com/mikedilger/nostr-types", rev = "099077afc81def588b8655f010a15d77baaff7ca", features = [ "speedy" ] }
nostr-types = { git = "https://github.com/mikedilger/nostr-types", rev = "ab8abe371b42e6ed54372c7f97eb2c78cd8ce1c4", features = [ "speedy" ] }
parking_lot = "0.12"
qrcode = { git = "https://github.com/mikedilger/qrcode-rust", rev = "519b77b3efa3f84961169b47d3de08c5ddd86548" }
rand = "0.8"
@ -71,6 +70,9 @@ url = "2.4"
vecmap-rs = "0.1"
zeroize = "1.6"
[target.'cfg(windows)'.dependencies]
normpath = "1.1"
# Force scrypt to build with release-like speed even in dev mode
[profile.dev.package.scrypt]
opt-level = 3

49
PERFORMANCE.md Normal file
View File

@ -0,0 +1,49 @@
# Performance
It is possible to operate gossip in a way that causes it to perform poorly, to do lots of disk activity and for the UI to be poorly responsive. This isn't usually due to bugs, but is due to asking too much of gossip.
## Possible Causes
### Following too many people
Think about it. If you followed a million people and demanded that gossip load all their recent events and all the likes and zaps on those events, it is going to take a long time to do that.
This is also true if you follow 1000 people that post a lot at 4x relay redundancy. You may need to load 20,000 events before gossip settles down.
This issue will be ameliorated somewhat in the future when you can have different feeds each with different groups of people.
### Slow Hardware
*CPU* - If when gossip seems busy and poorly responsive your CPU is at 100%, then you are CPU bound.
*Disks* - If you are running on a physical spinning disk, this will be a lot slower than when running on an SSD. I highly recommend using an SSD. (However, I run gossip on physical disks in order to help me discover and fix performance issues).
### Aggressive Settings
*feed chunk* - Your feed chunk may be too long, meaning gossip is seeking to load too many events.
*replies chunk* - Similar to feed chunk when looking at your inbox
*number of relays per person* - This should be two. Three is much more expensive than two.
*max relays* - I would put this down around 25 or lower if you are having performance problems.
*max fps* - The FPS needs to be no higher than 10. FPS of 7 is reasonable. High fps is very expensive for very little benefit (unless you are not having performance problems and want a super smooth experience).
*recompute feed periodically* - I would turn this off and press refresh manually once gossip has settled down.
*reactions* - Reactions are a lot of events with low value. I would turn off reactions if I had performance problems.
*load media* - You could save some processing by not loading media automatically. You'll have the option to click to load.
### Non-Optimized Compile
Gossip should be compiled in release mode. You can also compile it for your individual processor to squeeze out the most performance (the following line leaves out feature flags, you'll wnat to determine which ones are right for you):
````bash
$ RUSTFLAGS="-C target-cpu=native --cfg tokio_unstable" cargo build --release
````
### Dumb Programmers
Yes, I am recursively pulling my head out of my ass and left to wonder, "is it asses all the way down?"

View File

@ -84,6 +84,7 @@ Gossip is ready to use as a daily client if you wish. There are shortcomings, an
- [ ] NIP-40 - Expiration Timestamp
- [x] NIP-42 - Authentication of clients to relays
- [ ] NIP-46 - Nostr Connect
- [x] NIP-48 - Proxy Tags
- [ ] NIP-50 - Keywords filter
- [ ] NIP-51 - Lists
- [ ] NIP-56 - Reporting
@ -116,6 +117,7 @@ Most dependencies are probably already installed in your base operating system.
- pkg-config (debian: "pkg-config")
- openssl (debian: "libssl-dev")
- fontconfig (debian: "libfontconfig1-dev")
- ffmpeg support (debian: libavutil-dev libavformat-dev libavfilter-dev libavdevice-dev libxext-dev libclang-dev)
#### macOS
@ -211,6 +213,10 @@ Compile with
## Known Issues
### Performance issues
If you are having performance issues, please see [PERFORMANCE.md](PERFORMANCE.md).
### Sqlite Constraint Issues (Foreign or Unique Key)
First you need to locate your database file. The gossip directory is under this path: https://docs.rs/dirs/4.0.0/dirs/fn.data_dir.html The database file is `gossip.sqlite`. Then you need to install `sqlite3` on your system.

View File

@ -0,0 +1,100 @@
use heed::types::UnalignedSlice;
use heed::{EnvFlags, EnvOpenOptions};
use nostr_types::{PublicKey, RelayUrl};
use speedy::{Readable, Writable};
use std::{env, fmt};
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = env::args();
let _ = args.next(); // program name
let pubkeyhex = match args.next() {
Some(data) => data,
None => panic!("Usage: dump_person_relays <PublicKeyHex>"),
};
let pubkey = PublicKey::try_from_hex_string(&pubkeyhex, true).unwrap();
let mut builder = EnvOpenOptions::new();
unsafe {
builder.flags(EnvFlags::NO_SYNC);
}
builder.max_dbs(32);
builder.map_size(1048576 * 1024 * 24); // 24 GB
let pathbuf = PathBuf::from("/home/mike/.local/share/gossip/unstable/lmdb");
let env = builder.open(pathbuf.as_path())?;
let mut txn = env.write_txn()?;
let person_relays = env
.database_options()
.types::<UnalignedSlice<u8>, UnalignedSlice<u8>>()
.name("person_relays")
.create(&mut txn)?;
txn.commit()?;
let start_key = pubkey.to_bytes();
let txn = env.read_txn()?;
let iter = person_relays.prefix_iter(&txn, &start_key)?;
let mut output: Vec<PersonRelay> = Vec::new();
for result in iter {
let (_key, val) = result?;
let person_relay = PersonRelay::read_from_buffer(val)?;
output.push(person_relay);
}
for pr in &output {
println!("{}", pr);
}
Ok(())
}
#[derive(Debug, Readable, Writable)]
pub struct PersonRelay {
// The person
pub pubkey: PublicKey,
// The relay associated with that person
pub url: RelayUrl,
// The last time we fetched one of the person's events from this relay
pub last_fetched: Option<u64>,
// When we follow someone at a relay
pub last_suggested_kind3: Option<u64>,
// When we get their nip05 and it specifies this relay
pub last_suggested_nip05: Option<u64>,
// Updated when a 'p' tag on any event associates this person and relay via the
// recommended_relay_url field
pub last_suggested_bytag: Option<u64>,
pub read: bool,
pub write: bool,
// When we follow someone at a relay, this is set true
pub manually_paired_read: bool,
// When we follow someone at a relay, this is set true
pub manually_paired_write: bool,
}
impl fmt::Display for PersonRelay {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"{{ pubkey: {}, write: {}, read: {}, url: {}, last_fetched: {:?}, last_suggested_kind3: {:?}, last_suggested_nip05: {:?}, last_suggested_bytag: {:?}, manually_paired_read: {}, manually_paired_write: {} }}",
self.pubkey.as_hex_string(),
self.write,
self.read,
self.url,
self.last_fetched,
self.last_suggested_kind3,
self.last_suggested_nip05,
self.last_suggested_bytag,
self.manually_paired_read,
self.manually_paired_write
)
}
}

View File

@ -61,7 +61,7 @@ impl Delegation {
pub async fn save_through_settings(&self) -> Result<(), Error> {
GLOBALS.settings.write().delegatee_tag = self.get_delegatee_tag_as_str();
let settings = GLOBALS.settings.read();
GLOBALS.storage.write_settings(&settings)?;
GLOBALS.storage.write_settings(&settings, None)?;
Ok(())
}
}

View File

@ -5,10 +5,11 @@ pub enum ErrorKind {
BroadcastSend(tokio::sync::broadcast::error::SendError<ToMinionMessage>),
BroadcastReceive(tokio::sync::broadcast::error::RecvError),
Delegation(String),
Empty(String),
General(String),
HttpError(http::Error),
JoinError(tokio::task::JoinError),
Lmdb(lmdb::Error),
Lmdb(heed::Error),
MaxRelaysReached,
MpscSend(tokio::sync::mpsc::error::SendError<ToOverlordMessage>),
Nip05KeyNotFound,
@ -25,6 +26,7 @@ pub enum ErrorKind {
ParseInt(std::num::ParseIntError),
Regex(regex::Error),
RelayPickerError(gossip_relay_picker::Error),
RelayRejectedUs,
ReqwestHttpError(reqwest::Error),
Sql(rusqlite::Error),
SerdeJson(serde_json::Error),
@ -58,6 +60,7 @@ impl std::fmt::Display for Error {
BroadcastSend(e) => write!(f, "Error broadcasting: {e}"),
BroadcastReceive(e) => write!(f, "Error receiving broadcast: {e}"),
Delegation(s) => write!(f, "NIP-26 Delegation Error: {s}"),
Empty(s) => write!(f, "{s} is empty"),
General(s) => write!(f, "{s}"),
HttpError(e) => write!(f, "HTTP error: {e}"),
JoinError(e) => write!(f, "Task join error: {e}"),
@ -81,6 +84,7 @@ impl std::fmt::Display for Error {
ParseInt(e) => write!(f, "Bad integer: {e}"),
Regex(e) => write!(f, "Regex: {e}"),
RelayPickerError(e) => write!(f, "Relay Picker error: {e}"),
RelayRejectedUs => write!(f, "Relay rejected us."),
ReqwestHttpError(e) => write!(f, "HTTP (reqwest) error: {e}"),
Sql(e) => write!(f, "SQL: {e}"),
SerdeJson(e) => write!(f, "SerdeJson Error: {e}"),
@ -194,8 +198,8 @@ impl From<http::uri::InvalidUri> for ErrorKind {
}
}
impl From<lmdb::Error> for ErrorKind {
fn from(e: lmdb::Error) -> ErrorKind {
impl From<heed::Error> for ErrorKind {
fn from(e: heed::Error) -> ErrorKind {
ErrorKind::Lmdb(e)
}
}

View File

@ -44,7 +44,7 @@ impl Feed {
followed_feed: RwLock::new(Vec::new()),
inbox_feed: RwLock::new(Vec::new()),
person_feed: RwLock::new(Vec::new()),
interval_ms: RwLock::new(1000), // Every second, until we load from settings
interval_ms: RwLock::new(10000), // Every 10 seconds, until we load from settings
last_computed: RwLock::new(None),
thread_parent: RwLock::new(None),
}
@ -239,7 +239,6 @@ impl Feed {
// Filter further for the general feed
let dismissed = GLOBALS.dismissed.read().await.clone();
let now = Unixtime::now().unwrap();
let one_month_ago = now - Duration::new(60 * 60 * 24 * 30, 0);
let current_feed_kind = self.current_feed_kind.read().to_owned();
match current_feed_kind {
@ -291,9 +290,11 @@ impl Feed {
None, // since
)?;
let since = now - Duration::from_secs(GLOBALS.settings.read().replies_chunk);
let inbox_events: Vec<Id> = GLOBALS
.storage
.read_events_referencing_person(&my_pubkey, one_month_ago, |e| {
.read_events_referencing_person(&my_pubkey, since, |e| {
if e.created_at > now {
return false;
} // no future events
@ -344,12 +345,14 @@ impl Feed {
}
}
FeedKind::Person(person_pubkey) => {
let since = now - Duration::from_secs(GLOBALS.settings.read().person_feed_chunk);
let events: Vec<(Unixtime, Id)> = GLOBALS
.storage
.find_events(
&kinds, // feed kinds
&[], // any person (due to delegation condition) // FIXME GINA
Some(one_month_ago), // one year ago
&kinds, // feed kinds
&[], // any person (due to delegation condition) // FIXME
Some(since),
|e| {
if dismissed.contains(&e.id) {
return false;

View File

@ -26,11 +26,8 @@ pub enum FetchState {
#[derive(Debug, Default)]
pub struct Fetcher {
// we don't want new() to fail in lazy_static init, so we just mark it dead if there was an error
// on creation
dead: Option<String>,
cache_dir: PathBuf,
client: Client,
cache_dir: RwLock<PathBuf>,
client: RwLock<Option<Client>>,
// Here is where we store the current state of each URL being fetched
urls: RwLock<HashMap<Url, FetchState>>,
@ -44,49 +41,34 @@ pub struct Fetcher {
impl Fetcher {
pub fn new() -> Fetcher {
let connect_timeout = std::time::Duration::new(10, 0);
let timeout = std::time::Duration::new(15, 0);
let client = match Client::builder()
.gzip(true)
.brotli(true)
.deflate(true)
.connect_timeout(connect_timeout)
.timeout(timeout)
.build()
{
Ok(c) => c,
Err(e) => {
return Fetcher {
dead: Some(format!("{}", e)),
..Default::default()
}
}
};
let mut f: Fetcher = Fetcher {
client,
Fetcher {
..Default::default()
};
// Setup the cache directory
let cache_dir = match Profile::current() {
Ok(p) => p.cache_dir,
Err(_) => {
f.dead = Some("No Data Directory.".to_owned());
return f;
}
};
f.cache_dir = cache_dir;
f
}
}
pub fn start() {
pub fn start() -> Result<(), Error> {
// Setup the cache directory
*GLOBALS.fetcher.cache_dir.write().unwrap() = Profile::current()?.cache_dir;
// Create client
let connect_timeout =
std::time::Duration::new(GLOBALS.settings.read().fetcher_connect_timeout_sec, 0);
let timeout = std::time::Duration::new(GLOBALS.settings.read().fetcher_timeout_sec, 0);
*GLOBALS.fetcher.client.write().unwrap() = Some(
Client::builder()
.gzip(true)
.brotli(true)
.deflate(true)
.connect_timeout(connect_timeout)
.timeout(timeout)
.build()?,
);
// Setup periodic queue management
tokio::task::spawn(async {
let fetcher_looptime_ms = GLOBALS.settings.read().fetcher_looptime_ms;
tokio::task::spawn(async move {
loop {
// Every 1200 milliseconds...
tokio::time::sleep(Duration::from_millis(1200)).await;
tokio::time::sleep(Duration::from_millis(fetcher_looptime_ms)).await;
// Process the queue
GLOBALS.fetcher.process_queue().await;
@ -98,6 +80,8 @@ impl Fetcher {
}
}
});
Ok(())
}
pub fn requests_queued(&self) -> usize {
@ -121,9 +105,6 @@ impl Fetcher {
}
pub async fn process_queue(&self) {
if self.dead.is_some() {
return;
}
if GLOBALS.settings.read().offline {
return;
}
@ -150,7 +131,7 @@ impl Fetcher {
}
let load = self.fetch_host_load(&host);
if load >= 3 {
if load >= GLOBALS.settings.read().fetcher_max_requests_per_host {
continue; // We cannot overload any given host
}
@ -170,16 +151,19 @@ impl Fetcher {
}
/// This is where external code attempts to get the bytes of a file.
///
/// If it is missing: You'll get an `Ok(None)` response, but the fetcher will then
/// work in the background to try to make it available for a future call.
///
/// If it is available: You'll get `Ok(Some(bytes))`. This will read from the file system.
/// If you call it over and over rapidly (e.g. from the UI), it will read from the filesystem
/// over and over again, which is bad. So the UI caller should have it's own means of
/// caching the results from this call.
pub fn try_get(&self, url: &Url, max_age: Duration) -> Result<Option<Vec<u8>>, Error> {
// FIXME - this function is called synchronously, but it makes several
// file system calls. This might be pushing the limits of what we should
// be blocking on.
// Error if we are dead
if let Some(reason) = &self.dead {
return Err((format!("Fetcher is dead: {}", reason), file!(), line!()).into());
}
// Do not fetch if offline
if GLOBALS.settings.read().offline {
return Ok(None);
@ -262,15 +246,7 @@ impl Fetcher {
Ok(None)
}
pub async fn fetch(&self, url: Url) {
// Error if we are dead
if GLOBALS.fetcher.dead.is_some() {
// mark as failed
tracing::debug!("FETCH {url}: Failed: fetcher is dead");
self.urls.write().unwrap().insert(url, FetchState::Failed);
return;
}
async fn fetch(&self, url: Url) {
// Do not fetch if offline
if GLOBALS.settings.read().offline {
tracing::debug!("FETCH {url}: Failed: offline mode");
@ -304,7 +280,9 @@ impl Fetcher {
.insert(url.clone(), FetchState::InFlight);
// Fetch the resource
let client = GLOBALS.fetcher.client.clone(); // it is an Arc internally
// it is an Arc internally
let client = GLOBALS.fetcher.client.read().unwrap().clone().unwrap();
let mut req = client.get(&url.0);
if let Some(ref etag) = etag {
req = req.header("if-none-match", etag.to_owned());
@ -376,6 +354,19 @@ impl Fetcher {
let maybe_response = req.send().await;
let low_exclusion = GLOBALS
.settings
.read()
.fetcher_host_exclusion_on_low_error_secs;
let med_exclusion = GLOBALS
.settings
.read()
.fetcher_host_exclusion_on_med_error_secs;
let high_exclusion = GLOBALS
.settings
.read()
.fetcher_host_exclusion_on_high_error_secs;
// Deal with response errors
let response = match maybe_response {
Ok(r) => r,
@ -383,11 +374,16 @@ impl Fetcher {
if e.is_builder() {
finish(FailOutcome::Fail, "builder error", Some(e.into()), 0);
} else if e.is_timeout() {
finish(FailOutcome::Requeue, "timeout", Some(e.into()), 30);
finish(
FailOutcome::Requeue,
"timeout",
Some(e.into()),
low_exclusion,
);
} else if e.is_request() {
finish(FailOutcome::Fail, "request error", Some(e.into()), 0);
} else if e.is_connect() {
finish(FailOutcome::Fail, "connect error", Some(e.into()), 15);
finish(FailOutcome::Fail, "connect error", Some(e.into()), 0);
} else if e.is_body() {
finish(FailOutcome::Fail, "body error", Some(e.into()), 0);
} else if e.is_decode() {
@ -402,7 +398,12 @@ impl Fetcher {
// Deal with status codes
let status = response.status();
if status.is_informational() {
finish(FailOutcome::Requeue, "informational error", None, 30);
finish(
FailOutcome::Requeue,
"informational error",
None,
low_exclusion,
);
return;
} else if status.is_redirection() {
if status == StatusCode::NOT_MODIFIED {
@ -413,20 +414,23 @@ impl Fetcher {
}
return;
} else if status.is_server_error() {
finish(FailOutcome::Requeue, "server error", None, 300);
// Give the server time to recover
finish(FailOutcome::Requeue, "server error", None, high_exclusion);
return;
// give them 5 minutes, maybe the server will recover
} else if status.is_success() {
// fall through
} else {
match status {
StatusCode::REQUEST_TIMEOUT => {
finish(FailOutcome::Requeue, "request timeout", None, 30);
// give 30 seconds and try again
finish(FailOutcome::Requeue, "request timeout", None, low_exclusion);
}
StatusCode::TOO_MANY_REQUESTS => {
finish(FailOutcome::Requeue, "too many requests", None, 30);
// give 15 seconds and try again
finish(
FailOutcome::Requeue,
"too many requests",
None,
med_exclusion,
);
}
_ => {
finish(FailOutcome::Fail, &format!("{}", status), None, 0);
@ -493,7 +497,7 @@ impl Fetcher {
hex::encode(result)
};
let mut cache_file = self.cache_dir.clone();
let mut cache_file = self.cache_dir.read().unwrap().clone();
cache_file.push(hash);
cache_file
}

View File

@ -84,8 +84,12 @@ pub struct Globals {
/// UI status messages
pub status_queue: PRwLock<StatusQueue>,
/// How many data bytes have been read from the network, not counting overhead
pub bytes_read: AtomicUsize,
/// How many subscriptions are open and not yet at EOSE
pub open_subscriptions: AtomicUsize,
/// Delegation handling
pub delegation: Delegation,
@ -112,6 +116,9 @@ pub struct Globals {
/// LMDB storage
pub storage: Storage,
/// Events Processed
pub events_processed: AtomicU32,
}
lazy_static! {
@ -148,6 +155,7 @@ lazy_static! {
"Welcome to Gossip. Status messages will appear here. Click them to dismiss them.".to_owned()
)),
bytes_read: AtomicUsize::new(0),
open_subscriptions: AtomicUsize::new(0),
delegation: Delegation::default(),
media: Media::new(),
people_search_results: PRwLock::new(Vec::new()),
@ -157,6 +165,7 @@ lazy_static! {
current_zap: PRwLock::new(ZapState::None),
hashtag_regex: Regex::new(r"(?:^|\W)(#[\w\p{Extended_Pictographic}]+)(?:$|\W)").unwrap(),
storage,
events_processed: AtomicU32::new(0),
}
};
}

View File

@ -79,6 +79,9 @@ fn main() -> Result<(), Error> {
.with_env_filter(env_filter)
.init();
// Initialize storage
GLOBALS.storage.init()?;
// Load settings
let settings = GLOBALS.storage.read_settings()?.unwrap();
*GLOBALS.settings.write() = settings;
@ -108,6 +111,13 @@ fn main() -> Result<(), Error> {
// Wait for the async thread to complete
async_thread.join().unwrap();
// Sync storage again
if let Err(e) = GLOBALS.storage.sync() {
tracing::error!("{}", e);
} else {
tracing::info!("LMDB synced.");
}
Ok(())
}

View File

@ -1,7 +1,10 @@
use crate::globals::GLOBALS;
use dashmap::{DashMap, DashSet};
use eframe::egui::ColorImage;
use eframe::egui::{Color32, ColorImage};
use egui_extras::image::FitTo;
use image::imageops;
use image::imageops::FilterType;
use image::DynamicImage;
use nostr_types::{UncheckedUrl, Url};
use std::collections::HashSet;
use std::sync::atomic::Ordering;
@ -71,35 +74,25 @@ impl Media {
.pixels_per_point_times_100
.load(Ordering::Relaxed)
/ 100;
if let Ok(color_image) = load_image_bytes(&bytes) {
// Check for max size
if color_image.size[0] > 16384 || color_image.size[1] > 16384 {
tracing::warn!(
"Image ignored (a dimension is greater than 16384 pixels)"
);
match load_image_bytes(
&bytes, false, // don't crop square
size, // default size,
false, // don't force that size
false, // don't round
) {
Ok(color_image) => {
GLOBALS.media.image_temp.insert(aurl, color_image);
}
Err(_) => {
GLOBALS
.media
.failed_media
.write()
.await
.insert(aurl.to_unchecked_url());
} else {
GLOBALS.media.image_temp.insert(aurl, color_image);
}
} else if let Ok(color_image) = egui_extras::image::load_svg_bytes_with_size(
&bytes,
FitTo::Size(size, size),
) {
GLOBALS.media.image_temp.insert(aurl, color_image);
} else {
// this cannot recover without new metadata
GLOBALS
.media
.failed_media
.write()
.await
.insert(aurl.to_unchecked_url());
};
}
});
self.media_pending_processing.insert(url.clone());
None
@ -128,11 +121,10 @@ impl Media {
return None; // can recover if the setting is switched
}
match GLOBALS
.fetcher
.try_get(url, Duration::from_secs(60 * 60 * 24 * 3))
{
// cache expires in 3 days
match GLOBALS.fetcher.try_get(
url,
Duration::from_secs(60 * 60 * GLOBALS.settings.read().media_becomes_stale_hours),
) {
Ok(None) => None,
Ok(Some(bytes)) => {
self.data_temp.insert(url.clone(), bytes);
@ -150,33 +142,74 @@ impl Media {
}
}
fn load_image_bytes(image_bytes: &[u8]) -> Result<ColorImage, String> {
use image::{imageops, DynamicImage};
// Note: size is required for SVG which has no inherent size, even if we don't resize
pub(crate) fn load_image_bytes(
image_bytes: &[u8],
square: bool,
default_size: u32,
force_resize: bool,
round: bool,
) -> Result<ColorImage, String> {
let mut color_image = match image::load_from_memory(image_bytes) {
Ok(mut image) => {
image = adjust_orientation(image_bytes, image);
if square {
image = crop_square(image);
}
if force_resize || image.width() > 16384 || image.height() > 16384 {
// https://docs.rs/image/latest/image/imageops/enum.FilterType.html
let algo = match &*GLOBALS.settings.read().image_resize_algorithm {
"Nearest" => FilterType::Nearest,
"Triangle" => FilterType::Triangle,
"CatmullRom" => FilterType::CatmullRom,
"Gaussian" => FilterType::Gaussian,
"Lanczos3" => FilterType::Lanczos3,
_ => FilterType::Triangle,
};
let mut image = image::load_from_memory(image_bytes).map_err(|err| err.to_string())?;
// This preserves aspect ratio. The sizes represent bounds.
image = image.resize(default_size, default_size, algo);
}
let current_size = [image.width() as _, image.height() as _];
let image_buffer = image.into_rgba8();
let pixels = image_buffer.as_flat_samples();
ColorImage::from_rgba_unmultiplied(current_size, pixels.as_slice())
}
Err(_) => {
// With SVG, we set the size so no resize.
// And there is no exif orientation data.
egui_extras::image::load_svg_bytes_with_size(
image_bytes,
FitTo::Size(default_size, default_size),
)?
}
};
image = match get_orientation(image_bytes) {
if round {
round_image(&mut color_image);
}
Ok(color_image)
}
fn adjust_orientation(image_bytes: &[u8], image: DynamicImage) -> DynamicImage {
match get_orientation(image_bytes) {
1 => image,
2 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&image)),
3 => DynamicImage::ImageRgba8(imageops::rotate180(&image)),
4 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&image)),
5 => {
image = DynamicImage::ImageRgba8(imageops::rotate90(&image));
let image = DynamicImage::ImageRgba8(imageops::rotate90(&image));
DynamicImage::ImageRgba8(imageops::flip_horizontal(&image))
}
6 => DynamicImage::ImageRgba8(imageops::rotate90(&image)),
7 => {
image = DynamicImage::ImageRgba8(imageops::rotate270(&image));
let image = DynamicImage::ImageRgba8(imageops::rotate270(&image));
DynamicImage::ImageRgba8(imageops::flip_horizontal(&image))
}
8 => DynamicImage::ImageRgba8(imageops::rotate270(&image)),
_ => image,
};
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
Ok(ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()))
}
}
fn get_orientation(image_bytes: &[u8]) -> u32 {
@ -193,3 +226,60 @@ fn get_orientation(image_bytes: &[u8]) -> u32 {
}
1
}
fn crop_square(image: DynamicImage) -> DynamicImage {
let smaller = image.width().min(image.height());
if image.width() > smaller {
let excess = image.width() - smaller;
image.crop_imm(excess / 2, 0, image.width() - excess, image.height())
} else if image.height() > smaller {
let excess = image.height() - smaller;
image.crop_imm(0, excess / 2, image.width(), image.height() - excess)
} else {
image
}
}
fn round_image(image: &mut ColorImage) {
// The radius to the edge of of the avatar circle
let edge_radius = image.size[0] as f32 / 2.0;
let edge_radius_squared = edge_radius * edge_radius;
for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
// y coordinate
let uy = pixnum / image.size[0];
let y = uy as f32;
let y_offset = edge_radius - y;
// x coordinate
let ux = pixnum % image.size[0];
let x = ux as f32;
let x_offset = edge_radius - x;
// The radius to this pixel (may be inside or outside the circle)
let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
// If inside of the avatar circle
if pixel_radius_squared <= edge_radius_squared {
// squareroot to find how many pixels we are from the edge
let pixel_radius: f32 = pixel_radius_squared.sqrt();
let distance = edge_radius - pixel_radius;
// If we are within 1 pixel of the edge, we should fade, to
// antialias the edge of the circle. 1 pixel from the edge should
// be 100% of the original color, and right on the edge should be
// 0% of the original color.
if distance <= 1.0 {
*pixel = Color32::from_rgba_premultiplied(
(pixel.r() as f32 * distance) as u8,
(pixel.g() as f32 * distance) as u8,
(pixel.b() as f32 * distance) as u8,
(pixel.a() as f32 * distance) as u8,
);
}
} else {
// Outside of the avatar circle
*pixel = Color32::TRANSPARENT;
}
}
}

View File

@ -134,7 +134,7 @@ async fn update_relays(nip05: String, nip05file: Nip05, pubkey: &PublicKey) -> R
for relay in relays.iter() {
// Save relay
if let Ok(relay_url) = RelayUrl::try_from_unchecked_url(relay) {
GLOBALS.storage.write_relay_if_missing(&relay_url)?;
GLOBALS.storage.write_relay_if_missing(&relay_url, None)?;
// Save person_relay
let mut pr = match GLOBALS.storage.read_person_relay(*pubkey, &relay_url)? {
@ -142,7 +142,7 @@ async fn update_relays(nip05: String, nip05file: Nip05, pubkey: &PublicKey) -> R
None => PersonRelay::new(*pubkey, relay_url.clone()),
};
pr.last_suggested_nip05 = Some(Unixtime::now().unwrap().0 as u64);
GLOBALS.storage.write_person_relay(&pr)?;
GLOBALS.storage.write_person_relay(&pr, None)?;
}
}

View File

@ -21,46 +21,36 @@ impl Minion {
}
};
let mut maxtime = Unixtime::now()?;
maxtime.0 += 60 * 15; // 15 minutes into the future
match relay_message {
RelayMessage::Event(subid, event) => {
if let Err(e) = event.verify(Some(maxtime)) {
tracing::error!(
"{}: VERIFY ERROR: {}, {}",
&self.url,
e,
serde_json::to_string(&event)?
)
} else {
let handle = self
.subscription_map
.get_handle_by_id(&subid.0)
.unwrap_or_else(|| "_".to_owned());
let handle = self
.subscription_map
.get_handle_by_id(&subid.0)
.unwrap_or_else(|| "_".to_owned());
// Events that come in after EOSE on the general feed bump the last_general_eose
// timestamp for that relay, so we don't query before them next time we run.
if let Some(sub) = self.subscription_map.get_mut_by_id(&subid.0) {
if handle == "general_feed" && sub.eose() {
// Update last general EOSE
self.dbrelay.last_general_eose_at =
Some(match self.dbrelay.last_general_eose_at {
Some(old) => old.max(event.created_at.0 as u64),
None => event.created_at.0 as u64,
});
}
// Events that come in after EOSE on the general feed bump the last_general_eose
// timestamp for that relay, so we don't query before them next time we run.
if let Some(sub) = self.subscription_map.get_mut_by_id(&subid.0) {
if handle == "general_feed" && sub.eose() {
// Update last general EOSE
self.dbrelay.last_general_eose_at =
Some(match self.dbrelay.last_general_eose_at {
Some(old) => old.max(event.created_at.0 as u64),
None => event.created_at.0 as u64,
});
GLOBALS.storage.modify_relay(
&self.dbrelay.url,
|relay| {
relay.last_general_eose_at = self.dbrelay.last_general_eose_at;
},
None,
)?;
}
// Try processing everything immediately
crate::process::process_new_event(
&event,
true,
Some(self.url.clone()),
Some(handle),
)
.await?;
}
// Try processing everything immediately
crate::process::process_new_event(&event, Some(self.url.clone()), Some(handle))
.await?;
}
RelayMessage::Notice(msg) => {
tracing::warn!("{}: NOTICE: {}", &self.url, msg);
@ -91,6 +81,13 @@ impl Minion {
Some(old) => old.max(now),
None => now,
});
GLOBALS.storage.modify_relay(
&self.dbrelay.url,
|relay| {
relay.last_general_eose_at = self.dbrelay.last_general_eose_at;
},
None,
)?;
}
}
None => {
@ -122,6 +119,7 @@ impl Minion {
id,
&self.url,
Unixtime::now().unwrap(),
None,
)?;
} else {
// demerit the relay

View File

@ -52,7 +52,7 @@ impl Minion {
Some(dbrelay) => dbrelay,
None => {
let dbrelay = Relay::new(url.clone());
GLOBALS.storage.write_relay(&dbrelay)?;
GLOBALS.storage.write_relay(&dbrelay, None)?;
dbrelay
}
};
@ -77,6 +77,9 @@ impl Minion {
// minion will log when it connects
tracing::trace!("{}: Minion handling started", &self.url);
let fetcher_timeout =
std::time::Duration::new(GLOBALS.settings.read().fetcher_timeout_sec, 0);
// Connect to the relay
let websocket_stream = {
// Parse the URI
@ -94,7 +97,7 @@ impl Minion {
// Fetch NIP-11 data
let request_nip11_future = reqwest::Client::builder()
.timeout(std::time::Duration::new(30, 0))
.timeout(fetcher_timeout)
.redirect(reqwest::redirect::Policy::none())
.gzip(true)
.brotli(true)
@ -126,7 +129,12 @@ impl Minion {
"{}: Unable to parse response as NIP-11 ({}): {}\n",
&self.url,
e,
text.lines().take(10).collect::<Vec<_>>().join("\n")
text.lines()
.take(
GLOBALS.settings.read().nip11_lines_to_output_on_error
)
.collect::<Vec<_>>()
.join("\n")
);
}
}
@ -138,7 +146,7 @@ impl Minion {
}
// Save updated NIP-11 data (even if it failed)
GLOBALS.storage.write_relay(&self.dbrelay)?;
GLOBALS.storage.write_relay(&self.dbrelay, None)?;
let key: [u8; 16] = rand::random();
@ -182,17 +190,25 @@ impl Minion {
// Cameri nostream relay limits to 0.5 a megabyte
// Based on my current database of 7356 events, the longest was 11,121 bytes.
// Cameri said people with >2k followers were losing data at 128kb cutoff.
max_message_size: Some(1024 * 1024), // 1 MB
max_frame_size: Some(1024 * 1024), // 1 MB
accept_unmasked_frames: false, // default is false which is the standard
max_message_size: Some(
GLOBALS.settings.read().max_websocket_message_size_kb * 1024,
),
max_frame_size: Some(GLOBALS.settings.read().max_websocket_frame_size_kb * 1024),
accept_unmasked_frames: GLOBALS.settings.read().websocket_accept_unmasked_frames,
};
let (websocket_stream, _response) = tokio::time::timeout(
std::time::Duration::new(15, 0),
let connect_timeout = GLOBALS.settings.read().websocket_connect_timeout_sec;
let (websocket_stream, response) = tokio::time::timeout(
std::time::Duration::new(connect_timeout, 0),
tokio_tungstenite::connect_async_with_config(req, Some(config), false),
)
.await??;
// Check the status code of the response
if response.status().as_u16() == 4000 {
return Err(ErrorKind::RelayRejectedUs.into());
}
tracing::debug!("{}: Connected", &self.url);
websocket_stream
@ -245,14 +261,31 @@ impl Minion {
async fn loop_handler(&mut self) -> Result<(), Error> {
let ws_stream = self.stream.as_mut().unwrap();
let mut timer = tokio::time::interval(std::time::Duration::new(55, 0));
let mut timer = tokio::time::interval(std::time::Duration::new(
GLOBALS.settings.read().websocket_ping_frequency_sec,
0,
));
timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
timer.tick().await; // use up the first immediate tick.
select! {
biased;
_ = timer.tick() => {
ws_stream.send(WsMessage::Ping(vec![0x1])).await?;
},
to_minion_message = self.from_overlord.recv() => {
let to_minion_message = match to_minion_message {
Ok(m) => m,
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
self.keepgoing = false;
return Ok(());
},
Err(e) => return Err(e.into())
};
if to_minion_message.target == self.url.0 || to_minion_message.target == "all" {
self.handle_overlord_message(to_minion_message.payload).await?;
}
},
ws_message = ws_stream.next() => {
let ws_message = match ws_message {
Some(m) => m,
@ -284,19 +317,6 @@ impl Minion {
WsMessage::Frame(_) => tracing::warn!("{}: Unexpected frame message", &self.url),
}
},
to_minion_message = self.from_overlord.recv() => {
let to_minion_message = match to_minion_message {
Ok(m) => m,
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
self.keepgoing = false;
return Ok(());
},
Err(e) => return Err(e.into())
};
if to_minion_message.target == self.url.0 || to_minion_message.target == "all" {
self.handle_overlord_message(to_minion_message.payload).await?;
}
},
}
// Don't continue if we have no more subscriptions
@ -659,10 +679,9 @@ impl Minion {
// NOTE we do not unsubscribe to the general feed
// Allow all feed related event kinds
let mut event_kinds = GLOBALS.settings.read().feed_related_event_kinds();
// Exclude DMs and reactions (we wouldn't see the post it reacted to) in person feed
event_kinds
.retain(|f| *f != EventKind::EncryptedDirectMessage && *f != EventKind::Reaction);
let mut event_kinds = GLOBALS.settings.read().feed_displayable_event_kinds();
// Exclude DMs
event_kinds.retain(|f| *f != EventKind::EncryptedDirectMessage);
let filters: Vec<Filter> = vec![Filter {
authors: vec![Into::<PublicKeyHex>::into(pubkey).prefix(16)],
@ -726,8 +745,6 @@ impl Minion {
let mut filters: Vec<Filter> = Vec::new();
let enable_reactions = GLOBALS.settings.read().reactions;
if !vec_ids.is_empty() {
let idhp: Vec<IdHexPrefix> = vec_ids
.iter()
@ -743,10 +760,7 @@ impl Minion {
});
// Get reactions to ancestors, but not replies
let mut kinds = vec![EventKind::EventDeletion];
if enable_reactions {
kinds.push(EventKind::Reaction);
}
let kinds = GLOBALS.settings.read().feed_augment_event_kinds();
filters.push(Filter {
e: vec_ids,
kinds,
@ -1071,7 +1085,7 @@ impl Minion {
self.dbrelay.failure_count += 1;
// Save to storage
if let Err(e) = GLOBALS.storage.write_relay(&self.dbrelay) {
if let Err(e) = GLOBALS.storage.write_relay(&self.dbrelay, None) {
tracing::error!("{}: ERROR bumping relay failure count: {}", &self.url, e);
}
}
@ -1086,7 +1100,7 @@ impl Minion {
}
// Save to storage
if let Err(e) = GLOBALS.storage.write_relay(&self.dbrelay) {
if let Err(e) = GLOBALS.storage.write_relay(&self.dbrelay, None) {
tracing::error!("{}: ERROR bumping relay success count: {}", &self.url, e);
}
}

View File

@ -1,20 +1,25 @@
use crate::globals::GLOBALS;
use nostr_types::{ClientMessage, Filter, SubscriptionId};
use std::sync::atomic::Ordering;
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct Subscription {
id: String,
job_id: u64,
filters: Vec<Filter>,
eose: bool,
clone: bool,
}
impl Subscription {
pub fn new(id: &str, job_id: u64) -> Subscription {
GLOBALS.open_subscriptions.fetch_add(1, Ordering::SeqCst);
Subscription {
id: id.to_owned(),
job_id,
filters: vec![],
eose: false,
clone: false,
}
}
@ -37,6 +42,9 @@ impl Subscription {
}
pub fn set_eose(&mut self) {
if !self.clone && !self.eose {
GLOBALS.open_subscriptions.fetch_sub(1, Ordering::SeqCst);
}
self.eose = true;
}
@ -52,3 +60,23 @@ impl Subscription {
ClientMessage::Close(SubscriptionId(self.get_id()))
}
}
impl Clone for Subscription {
fn clone(&self) -> Self {
Subscription {
id: self.id.clone(),
job_id: self.job_id,
filters: self.filters.clone(),
eose: self.eose,
clone: true,
}
}
}
impl Drop for Subscription {
fn drop(&mut self) {
if !self.clone && !self.eose {
GLOBALS.open_subscriptions.fetch_sub(1, Ordering::SeqCst);
}
}
}

View File

@ -5,6 +5,7 @@ use crate::comms::{
};
use crate::error::{Error, ErrorKind};
use crate::globals::{ZapState, GLOBALS};
use crate::people::Person;
use crate::person_relay::PersonRelay;
use crate::relay::Relay;
use crate::tags::{
@ -15,8 +16,9 @@ use gossip_relay_picker::{Direction, RelayAssignment};
use http::StatusCode;
use minion::Minion;
use nostr_types::{
EncryptedPrivateKey, EventKind, Id, IdHex, Metadata, MilliSatoshi, NostrBech32, PayRequestData,
PreEvent, PrivateKey, Profile, PublicKey, RelayUrl, Tag, UncheckedUrl, Unixtime,
EncryptedPrivateKey, Event, EventKind, Id, IdHex, Metadata, MilliSatoshi, NostrBech32,
PayRequestData, PreEvent, PrivateKey, Profile, PublicKey, RelayUrl, Tag, UncheckedUrl,
Unixtime,
};
use std::collections::HashMap;
use std::sync::atomic::Ordering;
@ -106,7 +108,7 @@ impl Overlord {
pub async fn run_inner(&mut self) -> Result<(), Error> {
// Start the fetcher
crate::fetcher::Fetcher::start();
crate::fetcher::Fetcher::start()?;
// Load signer from settings
GLOBALS.signer.load_from_settings()?;
@ -385,7 +387,9 @@ impl Overlord {
Self::bump_failure_count(&url);
tracing::error!("Minion {} completed with error: {}", &url, e);
exclusion = 60;
if let ErrorKind::Websocket(wserror) = e.kind {
if let ErrorKind::RelayRejectedUs = e.kind {
exclusion = 60 * 60 * 24 * 365; // don't connect again, practically
} else if let ErrorKind::Websocket(wserror) = e.kind {
if let tungstenite::error::Error::Http(response) = wserror {
exclusion = match response.status() {
StatusCode::MOVED_PERMANENTLY => 60 * 60 * 24,
@ -468,15 +472,15 @@ impl Overlord {
fn bump_failure_count(url: &RelayUrl) {
if let Ok(Some(mut dbrelay)) = GLOBALS.storage.read_relay(url) {
dbrelay.failure_count += 1;
let _ = GLOBALS.storage.write_relay(&dbrelay);
let _ = GLOBALS.storage.write_relay(&dbrelay, None);
}
}
async fn handle_message(&mut self, message: ToOverlordMessage) -> Result<bool, Error> {
match message {
ToOverlordMessage::AddRelay(relay_str) => {
let dbrelay = Relay::new(relay_str.clone());
GLOBALS.storage.write_relay(&dbrelay)?;
let dbrelay = Relay::new(relay_str);
GLOBALS.storage.write_relay(&dbrelay, None)?;
}
ToOverlordMessage::ClearAllUsageOnRelay(relay_url) => {
if let Some(mut dbrelay) = GLOBALS.storage.read_relay(&relay_url)? {
@ -491,7 +495,7 @@ impl Overlord {
ToOverlordMessage::AdjustRelayUsageBit(relay_url, bit, value) => {
if let Some(mut dbrelay) = GLOBALS.storage.read_relay(&relay_url)? {
dbrelay.adjust_usage_bit(bit, value);
GLOBALS.storage.write_relay(&dbrelay)?;
GLOBALS.storage.write_relay(&dbrelay, None)?;
} else {
tracing::error!("CODE OVERSIGHT - We are adjusting a relay usage bit for a relay not in memory, how did that happen? It will not be saved.");
}
@ -576,7 +580,7 @@ impl Overlord {
ToOverlordMessage::HideOrShowRelay(relay_url, hidden) => {
if let Some(mut relay) = GLOBALS.storage.read_relay(&relay_url)? {
relay.hidden = hidden;
GLOBALS.storage.write_relay(&relay)?;
GLOBALS.storage.write_relay(&relay, None)?;
}
}
ToOverlordMessage::ImportPriv(mut import_priv, mut password) => {
@ -676,7 +680,8 @@ impl Overlord {
.write("Pruning database, please be patient..".to_owned());
let now = Unixtime::now().unwrap();
let then = now - Duration::new(60 * 60 * 24 * 180, 0); // 180 days
let then = now
- Duration::new(GLOBALS.settings.read().prune_period_days * 60 * 60 * 24, 0);
let count = GLOBALS.storage.prune(then)?;
GLOBALS.status_queue.write().write(format!(
@ -699,7 +704,7 @@ impl Overlord {
ToOverlordMessage::RankRelay(relay_url, rank) => {
if let Some(mut dbrelay) = GLOBALS.storage.read_relay(&relay_url)? {
dbrelay.rank = rank as u64;
GLOBALS.storage.write_relay(&dbrelay)?;
GLOBALS.storage.write_relay(&dbrelay, None)?;
}
}
ToOverlordMessage::ReengageMinion(url, persistent_jobs) => {
@ -713,7 +718,7 @@ impl Overlord {
}
ToOverlordMessage::SaveSettings => {
let settings = GLOBALS.settings.read().clone();
GLOBALS.storage.write_settings(&settings)?;
GLOBALS.storage.write_settings(&settings, None)?;
tracing::debug!("Settings saved.");
}
ToOverlordMessage::Search(text) => {
@ -744,7 +749,7 @@ impl Overlord {
let public_key = GLOBALS.signer.public_key().unwrap();
GLOBALS.settings.write().public_key = Some(public_key);
let settings = GLOBALS.settings.read().clone();
GLOBALS.storage.write_settings(&settings)?;
GLOBALS.storage.write_settings(&settings, None)?;
}
ToOverlordMessage::UpdateFollowing(merge) => {
self.update_following(merge).await?;
@ -868,7 +873,7 @@ impl Overlord {
// Save relay
let db_relay = Relay::new(relay.clone());
GLOBALS.storage.write_relay(&db_relay)?;
GLOBALS.storage.write_relay(&db_relay, None)?;
let now = Unixtime::now().unwrap().0 as u64;
@ -880,7 +885,7 @@ impl Overlord {
pr.last_suggested_kind3 = Some(now);
pr.manually_paired_read = true;
pr.manually_paired_write = true;
GLOBALS.storage.write_person_relay(&pr)?;
GLOBALS.storage.write_person_relay(&pr, None)?;
// async_follow added them to the relay tracker.
// Pick relays to start tracking them now
@ -1059,7 +1064,7 @@ impl Overlord {
};
// Process this event locally
crate::process::process_new_event(&event, false, None, None).await?;
crate::process::process_new_event(&event, None, None).await?;
// Determine which relays to post this to
let mut relay_urls: Vec<RelayUrl> = Vec::new();
@ -1260,7 +1265,7 @@ impl Overlord {
}
// Process the message for ourself
crate::process::process_new_event(&event, false, None, None).await?;
crate::process::process_new_event(&event, None, None).await?;
Ok(())
}
@ -1489,7 +1494,7 @@ impl Overlord {
};
// Process this event locally
crate::process::process_new_event(&event, false, None, None).await?;
crate::process::process_new_event(&event, None, None).await?;
// Determine which relays to post this to
let mut relay_urls: Vec<RelayUrl> = Vec::new();
@ -1668,7 +1673,7 @@ impl Overlord {
if let Ok(relay_url) = RelayUrl::try_from_unchecked_url(relay) {
// Save relay
let db_relay = Relay::new(relay_url.clone());
GLOBALS.storage.write_relay(&db_relay)?;
GLOBALS.storage.write_relay(&db_relay, None)?;
// Save person_relay
let mut pr = match GLOBALS
@ -1679,7 +1684,7 @@ impl Overlord {
None => PersonRelay::new(nprofile.pubkey, relay_url.clone()),
};
pr.last_suggested_nip05 = Some(Unixtime::now().unwrap().0 as u64);
GLOBALS.storage.write_person_relay(&pr)?;
GLOBALS.storage.write_person_relay(&pr, None)?;
}
}
@ -1738,7 +1743,7 @@ impl Overlord {
};
// Process this event locally
crate::process::process_new_event(&event, false, None, None).await?;
crate::process::process_new_event(&event, None, None).await?;
// Determine which relays to post this to
let mut relay_urls: Vec<RelayUrl> = Vec::new();
@ -1818,7 +1823,7 @@ impl Overlord {
.and_then(|rru| RelayUrl::try_from_unchecked_url(rru).ok())
{
// Save relay if missing
GLOBALS.storage.write_relay_if_missing(&url)?;
GLOBALS.storage.write_relay_if_missing(&url, None)?;
// create or update person_relay last_suggested_kind3
let mut pr = match GLOBALS.storage.read_person_relay(pubkey, &url)? {
@ -1826,7 +1831,7 @@ impl Overlord {
None => PersonRelay::new(pubkey, url.clone()),
};
pr.last_suggested_kind3 = Some(now.0 as u64);
GLOBALS.storage.write_person_relay(&pr)?;
GLOBALS.storage.write_person_relay(&pr, None)?;
}
// TBD: do something with the petname
}
@ -1842,7 +1847,9 @@ impl Overlord {
} else {
our_contact_list.created_at
};
GLOBALS.storage.write_last_contact_list_edit(last_edit.0)?;
GLOBALS
.storage
.write_last_contact_list_edit(last_edit.0, None)?;
// Pick relays again
{
@ -1864,21 +1871,67 @@ impl Overlord {
.write("You must enter at least 2 characters to search.".to_string());
return Ok(());
}
text = text.to_lowercase();
// If npub, convert to hex so we can find it in the database
// (This will only work with full npubs)
let mut pubkeytext = text.clone();
if let Ok(pk) = PublicKey::try_from_bech32_string(&text, true) {
pubkeytext = pk.as_hex_string();
let mut people_search_results: Vec<Person> = Vec::new();
let mut note_search_results: Vec<Event> = Vec::new();
// If a nostr: url, strip the 'nostr:' part
if text.len() >= 6 && &text[0..6] == "nostr:" {
text = text.split_off(6);
}
*GLOBALS.people_search_results.write() = GLOBALS.storage.filter_people(|p| {
if p.pubkey.as_hex_string().contains(&pubkeytext) {
return true;
if let Some(nb32) = NostrBech32::try_from_string(&text) {
match nb32 {
NostrBech32::EventAddr(ea) => {
if let Some(event) = GLOBALS
.storage
.find_events(
&[ea.kind],
&[ea.author],
None,
|event| {
event.tags.iter().any(|tag| {
if let Tag::Identifier { d, .. } = tag {
if *d == ea.d {
return true;
}
}
false
})
},
true,
)?
.get(1)
{
note_search_results.push(event.clone());
}
}
NostrBech32::EventPointer(ep) => {
if let Some(event) = GLOBALS.storage.read_event(ep.id)? {
note_search_results.push(event);
}
}
NostrBech32::Id(id) => {
if let Some(event) = GLOBALS.storage.read_event(id)? {
note_search_results.push(event);
}
}
NostrBech32::Profile(prof) => {
if let Some(person) = GLOBALS.storage.read_person(&prof.pubkey)? {
people_search_results.push(person);
}
}
NostrBech32::Pubkey(pk) => {
if let Some(person) = GLOBALS.storage.read_person(&pk)? {
people_search_results.push(person);
}
}
NostrBech32::Relay(_relay) => (),
}
}
people_search_results.extend(GLOBALS.storage.filter_people(|p| {
if let Some(metadata) = &p.metadata {
if let Ok(s) = serde_json::to_string(&metadata) {
if s.to_lowercase().contains(&text) {
@ -1894,9 +1947,11 @@ impl Overlord {
}
false
})?;
})?);
let note_search_results = GLOBALS.storage.search_events(&text)?;
note_search_results.extend(GLOBALS.storage.search_events(&text)?);
*GLOBALS.people_search_results.write() = people_search_results;
*GLOBALS.note_search_results.write() = note_search_results;
Ok(())

View File

@ -3,10 +3,8 @@ use crate::error::{Error, ErrorKind};
use crate::globals::GLOBALS;
use crate::AVATAR_SIZE;
use dashmap::{DashMap, DashSet};
use eframe::egui::{Color32, ColorImage};
use egui_extras::image::FitTo;
use eframe::egui::ColorImage;
use gossip_relay_picker::Direction;
use image::imageops::FilterType;
use nostr_types::{
Event, EventKind, Metadata, PreEvent, PublicKey, RelayUrl, Tag, UncheckedUrl, Unixtime, Url,
};
@ -51,7 +49,9 @@ impl Person {
}
pub fn display_name(&self) -> Option<&str> {
if let Some(md) = &self.metadata {
if let Some(pn) = &self.petname {
Some(pn)
} else if let Some(md) = &self.metadata {
if md.other.contains_key("display_name") {
if let Some(serde_json::Value::String(s)) = md.other.get("display_name") {
if !s.is_empty() {
@ -98,6 +98,29 @@ impl Person {
}
}
impl PartialEq for Person {
fn eq(&self, other: &Self) -> bool {
self.pubkey.eq(&other.pubkey)
}
}
impl Eq for Person {}
impl PartialOrd for Person {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match (self.display_name(), other.display_name()) {
(Some(a), Some(b)) => a.to_lowercase().partial_cmp(&b.to_lowercase()),
_ => self.pubkey.partial_cmp(&other.pubkey),
}
}
}
impl Ord for Person {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self.display_name(), other.display_name()) {
(Some(a), Some(b)) => a.to_lowercase().cmp(&b.to_lowercase()),
_ => self.pubkey.cmp(&other.pubkey),
}
}
}
pub struct People {
// active person's relays (pull from db as needed)
active_person: RwLock<Option<PublicKey>>,
@ -175,8 +198,11 @@ impl People {
task::spawn(async {
loop {
let fetch_metadata_looptime_ms =
GLOBALS.settings.read().fetcher_metadata_looptime_ms;
// Every 3 seconds...
tokio::time::sleep(Duration::from_millis(3000)).await;
tokio::time::sleep(Duration::from_millis(fetch_metadata_looptime_ms)).await;
// We fetch needed metadata
GLOBALS.people.maybe_fetch_metadata().await;
@ -201,12 +227,11 @@ impl People {
&self,
among_these: &[PublicKey],
) -> Vec<PublicKey> {
let one_day_ago = Unixtime::now().unwrap().0 - (60 * 60 * 8);
let stale = Unixtime::now().unwrap().0
- 60 * 60 * GLOBALS.settings.read().relay_list_becomes_stale_hours as i64;
if let Ok(vec) = GLOBALS.storage.filter_people(|p| {
p.followed
&& p.relay_list_last_received < one_day_ago
&& among_these.contains(&p.pubkey)
p.followed && p.relay_list_last_received < stale && among_these.contains(&p.pubkey)
}) {
vec.iter().map(|p| p.pubkey).collect()
} else {
@ -222,7 +247,7 @@ impl People {
pub fn create_all_if_missing(&self, pubkeys: &[PublicKey]) -> Result<(), Error> {
for pubkey in pubkeys {
GLOBALS.storage.write_person_if_missing(pubkey)?;
GLOBALS.storage.write_person_if_missing(pubkey, None)?;
}
Ok(())
@ -247,9 +272,11 @@ impl People {
let need = {
// Metadata refresh interval
let now = Unixtime::now().unwrap();
let eight_hours = Duration::from_secs(60 * 60 * 8);
let stale = Duration::from_secs(
60 * 60 * GLOBALS.settings.read().metadata_becomes_stale_hours,
);
person.metadata_created_at.is_none()
|| person.metadata_last_received < (now - eight_hours).0
|| person.metadata_last_received < (now - stale).0
};
if !need {
return;
@ -322,7 +349,7 @@ impl People {
// Update metadata_last_received, even if we don't update the metadata
person.metadata_last_received = now.0;
GLOBALS.storage.write_person(&person)?;
GLOBALS.storage.write_person(&person, None)?;
// Remove from the list of people that need metadata
self.need_metadata.remove(pubkey);
@ -347,9 +374,7 @@ impl People {
person.nip05_valid = false; // changed, so reset to invalid
person.nip05_last_checked = None; // we haven't checked this one yet
}
GLOBALS.storage.write_person(&person)?;
// UI cache invalidation (so notes of the person get rerendered)
GLOBALS.storage.write_person(&person, None)?;
GLOBALS.ui_people_to_invalidate.write().push(*pubkey);
}
@ -373,9 +398,16 @@ impl People {
} else if let Some(last) = person.nip05_last_checked {
// FIXME make these settings
let recheck_duration = if person.nip05_valid {
Duration::from_secs(60 * 60 * 24 * 14)
Duration::from_secs(
60 * 60 * GLOBALS.settings.read().nip05_becomes_stale_if_valid_hours,
)
} else {
Duration::from_secs(60 * 60 * 24)
Duration::from_secs(
60 * GLOBALS
.settings
.read()
.nip05_becomes_stale_if_invalid_minutes,
)
};
Unixtime::now().unwrap() - Unixtime(last as i64) > recheck_duration
} else {
@ -443,10 +475,10 @@ impl People {
}
};
match GLOBALS
.fetcher
.try_get(&url, Duration::from_secs(60 * 60 * 24 * 3))
{
match GLOBALS.fetcher.try_get(
&url,
Duration::from_secs(60 * 60 * GLOBALS.settings.read().avatar_becomes_stale_hours),
) {
// cache expires in 3 days
Ok(None) => None,
Ok(Some(bytes)) => {
@ -458,54 +490,23 @@ impl People {
.pixels_per_point_times_100
.load(Ordering::Relaxed)
/ 100;
if let Ok(mut image) = image::load_from_memory(&bytes) {
// Note: we can't use egui_extras::image::load_image_bytes because we
// need to modify the image
// Crop square
let smaller = image.width().min(image.height());
if image.width() > smaller {
let excess = image.width() - smaller;
image = image.crop_imm(
excess / 2,
0,
image.width() - excess,
image.height(),
);
} else if image.height() > smaller {
let excess = image.height() - smaller;
image = image.crop_imm(
0,
excess / 2,
image.width(),
image.height() - excess,
);
}
let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
let mut color_image = ColorImage::from_rgba_unmultiplied(
[
image_buffer.width() as usize,
image_buffer.height() as usize,
],
image_buffer.as_flat_samples().as_slice(),
);
if GLOBALS.settings.read().theme.round_image() {
round_image(&mut color_image);
}
GLOBALS.people.avatars_temp.insert(apubkey, color_image);
} else if let Ok(mut color_image) = egui_extras::image::load_svg_bytes_with_size(
let round_image = GLOBALS.settings.read().theme.round_image();
match crate::media::load_image_bytes(
&bytes,
FitTo::Size(size, size),
true, // crop square
size, // default size,
true, // force to that size
round_image,
) {
if GLOBALS.settings.read().theme.round_image() {
round_image(&mut color_image);
Ok(color_image) => {
GLOBALS.people.avatars_temp.insert(apubkey, color_image);
}
GLOBALS.people.avatars_temp.insert(apubkey, color_image);
} else {
// this cannot recover without new metadata
GLOBALS.failed_avatars.write().await.insert(apubkey);
};
Err(_) => {
// this cannot recover without new metadata
GLOBALS.failed_avatars.write().await.insert(apubkey);
}
}
});
self.avatars_pending_processing.insert(pubkey.to_owned());
None
@ -620,7 +621,7 @@ impl People {
// We don't use the data, but we shouldn't clobber it.
let content = match GLOBALS.storage.fetch_contact_list(&public_key)? {
Some(c) => c.content.clone(),
Some(c) => c.content,
None => "".to_owned(),
};
@ -643,7 +644,8 @@ impl People {
pub fn follow(&self, pubkey: &PublicKey, follow: bool) -> Result<(), Error> {
if let Some(mut person) = GLOBALS.storage.read_person(pubkey)? {
person.followed = follow;
GLOBALS.storage.write_person(&person)?;
GLOBALS.storage.write_person(&person, None)?;
GLOBALS.ui_people_to_invalidate.write().push(*pubkey);
}
Ok(())
}
@ -654,7 +656,8 @@ impl People {
if let Some(mut person) = GLOBALS.storage.read_person(pubkey)? {
if !person.followed {
person.followed = true;
GLOBALS.storage.write_person(&person)?;
GLOBALS.storage.write_person(&person, None)?;
GLOBALS.ui_people_to_invalidate.write().push(*pubkey);
}
}
}
@ -663,7 +666,8 @@ impl People {
let orig = person.followed;
person.followed = pubkeys.contains(&person.pubkey);
if person.followed != orig {
GLOBALS.storage.write_person(&person)?;
GLOBALS.storage.write_person(&person, None)?;
GLOBALS.ui_people_to_invalidate.write().push(person.pubkey);
}
}
}
@ -679,7 +683,8 @@ impl People {
pub fn follow_none(&self) -> Result<(), Error> {
for mut person in GLOBALS.storage.filter_people(|_| true)? {
person.followed = false;
GLOBALS.storage.write_person(&person)?;
GLOBALS.storage.write_person(&person, None)?;
GLOBALS.ui_people_to_invalidate.write().push(person.pubkey);
}
Ok(())
@ -688,7 +693,8 @@ impl People {
pub fn mute(&self, pubkey: &PublicKey, mute: bool) -> Result<(), Error> {
if let Some(mut person) = GLOBALS.storage.read_person(pubkey)? {
person.muted = mute;
GLOBALS.storage.write_person(&person)?;
GLOBALS.storage.write_person(&person, None)?;
GLOBALS.ui_people_to_invalidate.write().push(*pubkey);
}
// UI cache invalidation (so notes of the person get rerendered)
@ -726,7 +732,7 @@ impl People {
person.relay_list_created_at = Some(created_at);
}
GLOBALS.storage.write_person(&person)?;
GLOBALS.storage.write_person(&person, None)?;
Ok(retval)
}
@ -736,7 +742,7 @@ impl People {
if let Some(mut person) = GLOBALS.storage.read_person(&pubkey)? {
person.nip05_last_checked = Some(now as u64);
GLOBALS.storage.write_person(&person)?;
GLOBALS.storage.write_person(&person, None)?;
}
Ok(())
@ -752,24 +758,19 @@ impl People {
// Update memory
if let Some(mut person) = GLOBALS.storage.read_person(pubkey)? {
if let Some(metadata) = &mut person.metadata {
metadata.nip05 = nip05.clone()
metadata.nip05 = nip05
} else {
let mut metadata = Metadata::new();
metadata.nip05 = nip05.clone();
metadata.nip05 = nip05;
person.metadata = Some(metadata);
}
person.nip05_valid = nip05_valid;
person.nip05_last_checked = Some(nip05_last_checked);
GLOBALS.storage.write_person(&person)?;
GLOBALS.storage.write_person(&person, None)?;
GLOBALS.ui_people_to_invalidate.write().push(*pubkey);
}
// UI cache invalidation (so notes of the person get rerendered)
GLOBALS
.ui_people_to_invalidate
.write()
.push(pubkey.to_owned());
Ok(())
}
@ -793,50 +794,6 @@ impl People {
}
}
fn round_image(image: &mut ColorImage) {
// The radius to the edge of of the avatar circle
let edge_radius = image.size[0] as f32 / 2.0;
let edge_radius_squared = edge_radius * edge_radius;
for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
// y coordinate
let uy = pixnum / image.size[0];
let y = uy as f32;
let y_offset = edge_radius - y;
// x coordinate
let ux = pixnum % image.size[0];
let x = ux as f32;
let x_offset = edge_radius - x;
// The radius to this pixel (may be inside or outside the circle)
let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
// If inside of the avatar circle
if pixel_radius_squared <= edge_radius_squared {
// squareroot to find how many pixels we are from the edge
let pixel_radius: f32 = pixel_radius_squared.sqrt();
let distance = edge_radius - pixel_radius;
// If we are within 1 pixel of the edge, we should fade, to
// antialias the edge of the circle. 1 pixel from the edge should
// be 100% of the original color, and right on the edge should be
// 0% of the original color.
if distance <= 1.0 {
*pixel = Color32::from_rgba_premultiplied(
(pixel.r() as f32 * distance) as u8,
(pixel.g() as f32 * distance) as u8,
(pixel.b() as f32 * distance) as u8,
(pixel.a() as f32 * distance) as u8,
);
}
} else {
// Outside of the avatar circle
*pixel = Color32::TRANSPARENT;
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Nip05Patch {
nip05: Option<String>,

View File

@ -49,7 +49,8 @@ impl PersonRelay {
}
}
// This ranks the relays that a person writes to
// This ranks the relays that a person writes to, but does not consider local
// factors such as our relay rank or the success rate of the relay.
pub fn write_rank(mut dbprs: Vec<PersonRelay>) -> Vec<(RelayUrl, u64)> {
let now = Unixtime::now().unwrap().0 as u64;
let mut output: Vec<(RelayUrl, u64)> = Vec::new();
@ -106,7 +107,8 @@ impl PersonRelay {
output
}
// This ranks the relays that a person reads from
// This ranks the relays that a person reads from, but does not consider local
// factors such as our relay rank or the success rate of the relay.
pub fn read_rank(mut dbprs: Vec<PersonRelay>) -> Vec<(RelayUrl, u64)> {
let now = Unixtime::now().unwrap().0 as u64;
let mut output: Vec<(RelayUrl, u64)> = Vec::new();

View File

@ -2,7 +2,6 @@ use crate::comms::ToOverlordMessage;
use crate::error::Error;
use crate::globals::GLOBALS;
use crate::person_relay::PersonRelay;
use crate::relay::Relay;
use nostr_types::{
Event, EventKind, Metadata, NostrBech32, PublicKey, RelayUrl, SimpleRelayList, Tag, Unixtime,
};
@ -12,7 +11,6 @@ use std::sync::atomic::Ordering;
// and also populating the GLOBALS maps.
pub async fn process_new_event(
event: &Event,
from_relay: bool,
seen_on: Option<RelayUrl>,
subscription: Option<String>,
) -> Result<(), Error> {
@ -20,15 +18,17 @@ pub async fn process_new_event(
// Save seen-on-relay information
if let Some(url) = &seen_on {
if from_relay {
if seen_on.is_some() {
GLOBALS
.storage
.add_event_seen_on_relay(event.id, url, now)?;
.add_event_seen_on_relay(event.id, url, now, None)?;
}
}
GLOBALS.events_processed.fetch_add(1, Ordering::SeqCst);
// Determine if we already had this event
let duplicate = GLOBALS.storage.read_event(event.id)?.is_some();
let duplicate = GLOBALS.storage.has_event(event.id)?;
if duplicate {
tracing::trace!(
"{}: Old Event: {} {:?} @{}",
@ -42,9 +42,22 @@ pub async fn process_new_event(
// Save event
// Bail if the event is an already-replaced replaceable event
if from_relay {
if let Some(ref relay_url) = seen_on {
// Verify the event
let mut maxtime = now;
maxtime.0 += GLOBALS.settings.read().future_allowance_secs as i64;
if let Err(e) = event.verify(Some(maxtime)) {
tracing::error!(
"{}: VERIFY ERROR: {}, {}",
relay_url,
e,
serde_json::to_string(&event)?
);
return Ok(());
}
if event.kind.is_replaceable() {
if !GLOBALS.storage.replace_event(event)? {
if !GLOBALS.storage.replace_event(event, None)? {
tracing::trace!(
"{}: Old Event: {} {:?} @{}",
seen_on.as_ref().map(|r| r.as_str()).unwrap_or("_"),
@ -55,7 +68,7 @@ pub async fn process_new_event(
return Ok(()); // This did not replace anything.
}
} else if event.kind.is_parameterized_replaceable() {
if !GLOBALS.storage.replace_parameterized_event(event)? {
if !GLOBALS.storage.replace_parameterized_event(event, None)? {
tracing::trace!(
"{}: Old Event: {} {:?} @{}",
seen_on.as_ref().map(|r| r.as_str()).unwrap_or("_"),
@ -67,7 +80,7 @@ pub async fn process_new_event(
}
} else {
// This will ignore if it is already there
GLOBALS.storage.write_event(event)?;
GLOBALS.storage.write_event(event, None)?;
}
}
@ -80,7 +93,7 @@ pub async fn process_new_event(
event.created_at
);
if from_relay {
if seen_on.is_some() {
// Create the person if missing in the database
GLOBALS.people.create_all_if_missing(&[event.pubkey])?;
@ -91,12 +104,9 @@ pub async fn process_new_event(
None => PersonRelay::new(event.pubkey, url.clone()),
};
pr.last_fetched = Some(now.0 as u64);
GLOBALS.storage.write_person_relay(&pr)?;
GLOBALS.storage.write_person_relay(&pr, None)?;
}
// Save the tags into event_tag table
GLOBALS.storage.write_event_tags(event)?;
for tag in event.tags.iter() {
match tag {
Tag::Event {
@ -104,7 +114,7 @@ pub async fn process_new_event(
..
} => {
if let Ok(url) = RelayUrl::try_from_unchecked_url(should_be_url) {
GLOBALS.storage.write_relay_if_missing(&url)?;
GLOBALS.storage.write_relay_if_missing(&url, None)?;
}
}
Tag::Pubkey {
@ -114,7 +124,7 @@ pub async fn process_new_event(
} => {
if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) {
if let Ok(url) = RelayUrl::try_from_unchecked_url(should_be_url) {
GLOBALS.storage.write_relay_if_missing(&url)?;
GLOBALS.storage.write_relay_if_missing(&url, None)?;
// Add person if missing
GLOBALS.people.create_all_if_missing(&[pubkey])?;
@ -125,7 +135,7 @@ pub async fn process_new_event(
None => PersonRelay::new(pubkey, url.clone()),
};
pr.last_suggested_bytag = Some(now.0 as u64);
GLOBALS.storage.write_person_relay(&pr)?;
GLOBALS.storage.write_person_relay(&pr, None)?;
}
}
}
@ -135,16 +145,18 @@ pub async fn process_new_event(
}
// Save event relationships (whether from a relay or not)
let invalid_ids = GLOBALS.storage.process_relationships_of_event(event)?;
let invalid_ids = GLOBALS
.storage
.process_relationships_of_event(event, None)?;
// Invalidate UI events indicated by those relationships
GLOBALS.ui_notes_to_invalidate.write().extend(&invalid_ids);
// Save event_hashtags
if from_relay {
if seen_on.is_some() {
let hashtags = event.hashtags();
for hashtag in hashtags {
GLOBALS.storage.add_hashtag(&hashtag, event.id)?;
GLOBALS.storage.add_hashtag(&hashtag, event.id, None)?;
}
}
@ -196,7 +208,7 @@ pub async fn process_new_event(
}
if event.kind == EventKind::RelayList {
process_relay_list(event).await?;
GLOBALS.storage.process_relay_list(event)?;
}
// If the content contains an nevent and we don't have it, fetch it from those relays
@ -227,107 +239,6 @@ pub async fn process_new_event(
Ok(())
}
async fn process_relay_list(event: &Event) -> Result<(), Error> {
// Update that we received the relay list (and optionally bump forward the date
// if this relay list happens to be newer)
let newer = GLOBALS
.people
.update_relay_list_stamps(event.pubkey, event.created_at.0)
.await?;
if !newer {
return Ok(());
}
// Enable special handling for our own relay list
let mut ours = false;
if let Some(pubkey) = GLOBALS.signer.public_key() {
if event.pubkey == pubkey {
ours = true;
tracing::info!("Processing our own relay list");
// clear all read/write flags from relays (will be added back below)
Relay::clear_all_relay_list_usage_bits()?;
}
}
let mut inbox_relays: Vec<RelayUrl> = Vec::new();
let mut outbox_relays: Vec<RelayUrl> = Vec::new();
for tag in event.tags.iter() {
if let Tag::Reference { url, marker, .. } = tag {
if let Ok(relay_url) = RelayUrl::try_from_unchecked_url(url) {
if let Some(m) = marker {
match &*m.trim().to_lowercase() {
"read" => {
// 'read' means inbox and not outbox
inbox_relays.push(relay_url.clone());
if ours {
if let Some(mut dbrelay) = GLOBALS.storage.read_relay(&relay_url)? {
// Update
dbrelay.set_usage_bits(Relay::INBOX);
dbrelay.clear_usage_bits(Relay::OUTBOX);
GLOBALS.storage.write_relay(&dbrelay)?;
} else {
// Insert missing relay
let mut dbrelay = Relay::new(relay_url.to_owned());
// Since we are creating, we add READ
dbrelay.set_usage_bits(Relay::INBOX | Relay::READ);
GLOBALS.storage.write_relay(&dbrelay)?;
}
}
}
"write" => {
// 'write' means outbox and not inbox
outbox_relays.push(relay_url.clone());
if ours {
if let Some(mut dbrelay) = GLOBALS.storage.read_relay(&relay_url)? {
// Update
dbrelay.set_usage_bits(Relay::OUTBOX);
dbrelay.clear_usage_bits(Relay::INBOX);
GLOBALS.storage.write_relay(&dbrelay)?;
} else {
// Create
let mut dbrelay = Relay::new(relay_url.to_owned());
// Since we are creating, we add WRITE
dbrelay.set_usage_bits(Relay::OUTBOX | Relay::WRITE);
GLOBALS.storage.write_relay(&dbrelay)?;
}
}
}
_ => {} // ignore unknown marker
}
} else {
// No marker means both inbox and outbox
inbox_relays.push(relay_url.clone());
outbox_relays.push(relay_url.clone());
if ours {
if let Some(mut dbrelay) = GLOBALS.storage.read_relay(&relay_url)? {
// Update
dbrelay.set_usage_bits(Relay::INBOX | Relay::OUTBOX);
GLOBALS.storage.write_relay(&dbrelay)?;
} else {
// Create
let mut dbrelay = Relay::new(relay_url.to_owned());
// Since we are creating, we add READ and WRITE
dbrelay.set_usage_bits(
Relay::INBOX | Relay::OUTBOX | Relay::READ | Relay::WRITE,
);
GLOBALS.storage.write_relay(&dbrelay)?;
}
}
}
}
}
}
GLOBALS
.storage
.set_relay_list(event.pubkey, inbox_relays, outbox_relays)?;
Ok(())
}
async fn process_somebody_elses_contact_list(event: &Event) -> Result<(), Error> {
// We don't keep their contacts or show to the user yet.
// We only process the contents for (non-standard) relay list information.
@ -359,7 +270,7 @@ async fn process_somebody_elses_contact_list(event: &Event) -> Result<(), Error>
}
GLOBALS
.storage
.set_relay_list(event.pubkey, inbox_relays, outbox_relays)?;
.set_relay_list(event.pubkey, inbox_relays, outbox_relays, None)?;
}
Ok(())

View File

@ -2,9 +2,12 @@ use crate::error::Error;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
#[cfg(windows)]
use normpath::PathExt;
lazy_static! {
static ref CURRENT: RwLock<Option<Profile>> = RwLock::new(None);
}
@ -32,21 +35,21 @@ impl Profile {
.ok_or::<Error>("Cannot find a directory to store application data.".into())?;
// Canonicalize (follow symlinks, resolve ".." paths)
let data_dir = fs::canonicalize(data_dir)?;
let data_dir = normalize(data_dir)?;
// Push "gossip" to data_dir, or override with GOSSIP_DIR
let base_dir = match env::var("GOSSIP_DIR") {
Ok(dir) => {
tracing::info!("Using GOSSIP_DIR: {}", dir);
// Note, this must pre-exist
fs::canonicalize(PathBuf::from(dir))?
normalize(dir)?
}
Err(_) => {
let mut base_dir = data_dir;
base_dir.push("gossip");
// We canonicalize here because gossip might be a link, but if it
// doesn't exist yet we have to just go with basedir
fs::canonicalize(base_dir.as_path()).unwrap_or(base_dir)
normalize(base_dir.as_path()).unwrap_or(base_dir)
}
};
@ -86,8 +89,14 @@ impl Profile {
};
let lmdb_dir = {
let mut lmdb_dir = base_dir.clone();
let mut lmdb_dir = profile_dir.clone();
lmdb_dir.push("lmdb");
// Windows syntax not compatible with lmdb:
if lmdb_dir.starts_with(r#"\\?\"#) {
lmdb_dir = lmdb_dir.strip_prefix(r#"\\?\"#).unwrap().to_path_buf();
}
lmdb_dir
};
@ -119,3 +128,13 @@ impl Profile {
Ok(created)
}
}
#[cfg(not(windows))]
fn normalize<P: AsRef<Path>>(path: P) -> Result<PathBuf, Error> {
Ok(fs::canonicalize(path)?)
}
#[cfg(windows)]
fn normalize<P: AsRef<Path>>(path: P) -> Result<PathBuf, Error> {
Ok(path.as_ref().normalize()?.into_path_buf())
}

View File

@ -22,8 +22,8 @@ impl Relay {
pub const READ: u64 = 1 << 0; // 1
pub const WRITE: u64 = 1 << 1; // 2
pub const ADVERTISE: u64 = 1 << 2; // 4
pub const INBOX: u64 = 1 << 3; // 8
pub const OUTBOX: u64 = 1 << 4; // 16
pub const INBOX: u64 = 1 << 3; // 8 this is 'read' of kind 10002
pub const OUTBOX: u64 = 1 << 4; // 16 this is 'write' of kind 10002
pub const DISCOVER: u64 = 1 << 5; // 32
pub fn new(url: RelayUrl) -> Relay {
@ -49,12 +49,6 @@ impl Relay {
self.usage_bits &= !bits;
}
pub fn clear_all_relay_list_usage_bits() -> Result<(), Error> {
GLOBALS.storage.modify_all_relays(|relay| {
relay.usage_bits &= Self::INBOX | Self::OUTBOX;
})
}
pub fn adjust_usage_bit(&mut self, bit: u64, value: bool) {
if value {
self.set_usage_bits(bit);

View File

@ -3,75 +3,173 @@ use nostr_types::{EventKind, PublicKey};
use serde::{Deserialize, Serialize};
use speedy::{Readable, Writable};
#[derive(Clone, Debug, Serialize, Deserialize, Readable, Writable)]
#[derive(Clone, Debug, Serialize, Deserialize, Readable, Writable, PartialEq)]
pub struct Settings {
pub feed_chunk: u64,
pub replies_chunk: u64,
pub overlap: u64,
pub num_relays_per_person: u8,
pub max_relays: u8,
// ID settings
pub public_key: Option<PublicKey>,
pub max_fps: u32,
pub recompute_feed_periodically: bool,
pub feed_recompute_interval_ms: u32,
pub pow: u8,
pub log_n: u8,
// Network settings
pub offline: bool,
pub theme: Theme,
pub set_client_tag: bool,
pub set_user_agent: bool,
pub override_dpi: Option<u32>,
pub reactions: bool,
pub reposts: bool,
pub show_long_form: bool,
pub show_mentions: bool,
pub show_media: bool,
pub load_avatars: bool,
pub load_media: bool,
pub check_nip05: bool,
pub direct_messages: bool,
pub automatically_fetch_metadata: bool,
// Relay settings
pub num_relays_per_person: u8,
pub max_relays: u8,
// Feed Settings
pub feed_chunk: u64,
pub replies_chunk: u64,
pub person_feed_chunk: u64,
pub overlap: u64,
// Event Selection
pub reposts: bool,
pub show_long_form: bool,
pub show_mentions: bool,
pub direct_messages: bool,
pub future_allowance_secs: u64,
// Event Content Settings
pub reactions: bool,
pub enable_zap_receipts: bool,
pub show_media: bool,
// Posting Settings
pub pow: u8,
pub set_client_tag: bool,
pub set_user_agent: bool,
pub delegatee_tag: String,
// UI settings
pub max_fps: u32,
pub recompute_feed_periodically: bool,
pub feed_recompute_interval_ms: u32,
pub theme: Theme,
pub override_dpi: Option<u32>,
pub highlight_unread_events: bool,
pub posting_area_at_top: bool,
pub enable_zap_receipts: bool,
pub status_bar: bool,
pub image_resize_algorithm: String,
// Staletime settings
pub relay_list_becomes_stale_hours: u64,
pub metadata_becomes_stale_hours: u64,
pub nip05_becomes_stale_if_valid_hours: u64,
pub nip05_becomes_stale_if_invalid_minutes: u64,
pub avatar_becomes_stale_hours: u64,
pub media_becomes_stale_hours: u64,
// Websocket settings
pub max_websocket_message_size_kb: usize,
pub max_websocket_frame_size_kb: usize,
pub websocket_accept_unmasked_frames: bool,
pub websocket_connect_timeout_sec: u64,
pub websocket_ping_frequency_sec: u64,
// HTTP settings
pub fetcher_metadata_looptime_ms: u64,
pub fetcher_looptime_ms: u64,
pub fetcher_connect_timeout_sec: u64,
pub fetcher_timeout_sec: u64,
pub fetcher_max_requests_per_host: usize,
pub fetcher_host_exclusion_on_low_error_secs: u64,
pub fetcher_host_exclusion_on_med_error_secs: u64,
pub fetcher_host_exclusion_on_high_error_secs: u64,
pub nip11_lines_to_output_on_error: usize,
// Database settings
pub prune_period_days: u64,
}
impl Default for Settings {
fn default() -> Settings {
Settings {
feed_chunk: 60 * 60 * 12, // 12 hours
replies_chunk: 60 * 60 * 24 * 7, // 1 week
overlap: 300, // 5 minutes
// ID settings
public_key: None,
log_n: 18,
// Network settings
offline: false,
load_avatars: true,
load_media: true,
check_nip05: true,
automatically_fetch_metadata: true,
// Relay settings
num_relays_per_person: 2,
max_relays: 50,
public_key: None,
// Feed settings
feed_chunk: 60 * 60 * 12, // 12 hours
replies_chunk: 60 * 60 * 24 * 7, // 1 week
person_feed_chunk: 60 * 60 * 24 * 30, // 1 month
overlap: 300, // 5 minutes
// Event Selection
reposts: true,
show_long_form: false,
show_mentions: true,
direct_messages: true,
future_allowance_secs: 60 * 15, // 15 minutes
// Event Content Settings
reactions: true,
enable_zap_receipts: true,
show_media: true,
// Posting settings
pow: 0,
set_client_tag: false,
set_user_agent: false,
delegatee_tag: String::new(),
// UI settings
max_fps: 12,
recompute_feed_periodically: true,
feed_recompute_interval_ms: 8000,
pow: 0,
offline: false,
theme: Theme {
variant: ThemeVariant::Default,
dark_mode: false,
follow_os_dark_mode: false,
},
set_client_tag: false,
set_user_agent: false,
override_dpi: None,
reactions: true,
reposts: true,
show_long_form: false,
show_mentions: true,
show_media: true,
load_avatars: true,
load_media: true,
check_nip05: true,
direct_messages: true,
automatically_fetch_metadata: true,
delegatee_tag: String::new(),
highlight_unread_events: true,
posting_area_at_top: true,
enable_zap_receipts: true,
status_bar: false,
image_resize_algorithm: "CatmullRom".to_owned(),
// Staletime settings
relay_list_becomes_stale_hours: 8,
metadata_becomes_stale_hours: 8,
nip05_becomes_stale_if_valid_hours: 8,
nip05_becomes_stale_if_invalid_minutes: 30, // 30 minutes
avatar_becomes_stale_hours: 8,
media_becomes_stale_hours: 8,
// Websocket settings
max_websocket_message_size_kb: 1024, // 1 MB
max_websocket_frame_size_kb: 1024, // 1 MB
websocket_accept_unmasked_frames: false,
websocket_connect_timeout_sec: 15,
websocket_ping_frequency_sec: 55,
// HTTP settings
fetcher_metadata_looptime_ms: 3000,
fetcher_looptime_ms: 1800,
fetcher_connect_timeout_sec: 15,
fetcher_timeout_sec: 30,
fetcher_max_requests_per_host: 3,
fetcher_host_exclusion_on_low_error_secs: 30,
fetcher_host_exclusion_on_med_error_secs: 60,
fetcher_host_exclusion_on_high_error_secs: 600,
nip11_lines_to_output_on_error: 10,
// Database settings
prune_period_days: 30,
}
}
}
@ -102,4 +200,11 @@ impl Settings {
.filter(|k| k.is_feed_displayable())
.collect()
}
pub fn feed_augment_event_kinds(&self) -> Vec<EventKind> {
self.enabled_event_kinds()
.drain(..)
.filter(|k| k.augments_feed_related())
.collect()
}
}

View File

@ -8,8 +8,6 @@ use nostr_types::{
use parking_lot::RwLock;
use tokio::task;
const DEFAULT_LOG_N: u8 = 18;
#[derive(Default)]
pub struct Signer {
public: RwLock<Option<PublicKey>>,
@ -31,10 +29,10 @@ impl Signer {
pub async fn save_through_settings(&self) -> Result<(), Error> {
GLOBALS.settings.write().public_key = *self.public.read();
let settings = GLOBALS.settings.read().clone();
GLOBALS.storage.write_settings(&settings)?;
GLOBALS.storage.write_settings(&settings, None)?;
let epk = self.encrypted.read().clone();
GLOBALS.storage.write_encrypted_private_key(&epk)?;
GLOBALS.storage.write_encrypted_private_key(&epk, None)?;
Ok(())
}
@ -70,7 +68,7 @@ impl Signer {
}
pub fn set_private_key(&self, pk: PrivateKey, pass: &str) -> Result<(), Error> {
*self.encrypted.write() = Some(pk.export_encrypted(pass, DEFAULT_LOG_N)?);
*self.encrypted.write() = Some(pk.export_encrypted(pass, GLOBALS.settings.read().log_n)?);
*self.public.write() = Some(pk.public_key());
*self.private.write() = Some(pk);
Ok(())
@ -88,7 +86,8 @@ impl Signer {
// If older version, re-encrypt with new version at default 2^18 rounds
if epk.version()? < 2 {
*self.encrypted.write() = Some(private.export_encrypted(pass, DEFAULT_LOG_N)?);
*self.encrypted.write() =
Some(private.export_encrypted(pass, GLOBALS.settings.read().log_n)?);
// and eventually save
task::spawn(async move {
if let Err(e) = GLOBALS.signer.save_through_settings().await {
@ -130,7 +129,7 @@ impl Signer {
Some(epk) => {
// Test password
let pk = epk.decrypt(old)?;
let epk = pk.export_encrypted(new, DEFAULT_LOG_N)?;
let epk = pk.export_encrypted(new, GLOBALS.settings.read().log_n)?;
*self.encrypted.write() = Some(epk);
task::spawn(async move {
if let Err(e) = GLOBALS.signer.save_through_settings().await {
@ -149,7 +148,7 @@ impl Signer {
pub fn generate_private_key(&self, pass: &str) -> Result<(), Error> {
let pk = PrivateKey::generate();
*self.encrypted.write() = Some(pk.export_encrypted(pass, DEFAULT_LOG_N)?);
*self.encrypted.write() = Some(pk.export_encrypted(pass, GLOBALS.settings.read().log_n)?);
*self.public.write() = Some(pk.public_key());
*self.private.write() = Some(pk);
Ok(())
@ -201,7 +200,7 @@ impl Signer {
// We have to regenerate encrypted private key because it may have fallen from
// medium to weak security. And then we need to save that
let epk = pk.export_encrypted(pass, DEFAULT_LOG_N)?;
let epk = pk.export_encrypted(pass, GLOBALS.settings.read().log_n)?;
*self.encrypted.write() = Some(epk);
*self.private.write() = Some(pk);
task::spawn(async move {
@ -226,7 +225,7 @@ impl Signer {
// We have to regenerate encrypted private key because it may have fallen from
// medium to weak security. And then we need to save that
let epk = pk.export_encrypted(pass, DEFAULT_LOG_N)?;
let epk = pk.export_encrypted(pass, GLOBALS.settings.read().log_n)?;
*self.encrypted.write() = Some(epk);
*self.private.write() = Some(pk);
task::spawn(async move {

View File

@ -13,8 +13,7 @@ impl Storage {
pub(super) fn import(&self) -> Result<(), Error> {
tracing::info!("Importing SQLITE data into LMDB...");
// Disable sync, we will sync when we are done.
self.disable_sync()?;
let mut txn = self.env.write_txn()?;
// Progress the legacy database to the endpoint first
let mut db = legacy::init_database()?;
@ -23,14 +22,16 @@ impl Storage {
// local settings
import_local_settings(&db, |epk: Option<EncryptedPrivateKey>, lcle: i64| {
self.write_encrypted_private_key(&epk)?;
self.write_last_contact_list_edit(lcle)
self.write_encrypted_private_key(&epk, Some(&mut txn))?;
self.write_last_contact_list_edit(lcle, Some(&mut txn))
})?;
tracing::info!("LMDB: imported local settings.");
// old table "settings"
// Copy settings (including local_settings)
import_settings(&db, |settings: &Settings| self.write_settings(settings))?;
import_settings(&db, |settings: &Settings| {
self.write_settings(settings, Some(&mut txn))
})?;
tracing::info!("LMDB: imported settings.");
// old table "event_relay"
@ -39,7 +40,7 @@ impl Storage {
let id = Id::try_from_hex_string(&id)?;
let relay_url = RelayUrl(url);
let time = Unixtime(seen as i64);
self.add_event_seen_on_relay(id, &relay_url, time)
self.add_event_seen_on_relay(id, &relay_url, time, Some(&mut txn))
})?;
tracing::info!("LMDB: imported event-seen-on-relay data.");
@ -47,7 +48,7 @@ impl Storage {
// Copy event_flags
import_event_flags(&db, |id: Id, viewed: bool| {
if viewed {
self.mark_event_viewed(id)
self.mark_event_viewed(id, Some(&mut txn))
} else {
Ok(())
}
@ -58,43 +59,47 @@ impl Storage {
// Copy event_hashtags
import_hashtags(&db, |hashtag: String, event: String| {
let id = Id::try_from_hex_string(&event)?;
self.add_hashtag(&hashtag, id)
if let Err(e) = self.add_hashtag(&hashtag, id, Some(&mut txn)) {
tracing::error!("{}", e); // non fatal, keep importing
}
Ok(())
})?;
tracing::info!("LMDB: imported event hashtag index.");
// old table "relay"
// Copy relays
import_relays(&db, |dbrelay: &Relay| self.write_relay(dbrelay))?;
import_relays(&db, |dbrelay: &Relay| {
self.write_relay(dbrelay, Some(&mut txn))
})?;
tracing::info!("LMDB: imported relays.");
// old table "event"
// old table "event_tag"
// Copy events (and regenerate event_tags)
import_events(&db, |event: &Event| {
self.write_event(event)?;
self.write_event_tags(event)
})?;
// Copy events
import_events(&db, |event: &Event| self.write_event(event, Some(&mut txn)))?;
tracing::info!("LMDB: imported events and tag index");
// old table "person"
// Copy people
import_people(&db, |person: &Person| self.write_person(person))?;
import_people(&db, |person: &Person| {
self.write_person(person, Some(&mut txn))
})?;
tracing::info!("LMDB: imported people");
// old table "person_relay"
// Copy person relay
import_person_relays(&db, |person_relay: &PersonRelay| {
self.write_person_relay(person_relay)
self.write_person_relay(person_relay, Some(&mut txn))
})?;
tracing::info!("LMDB: import person_relays");
// Re-enable sync (it also syncs the data).
txn.commit()?;
self.sync()?;
// If we have a system crash before the migration level
// is written in the next line, import will start over.
self.enable_sync()?;
// Mark migration level
self.write_migration_level(0)?;
self.write_migration_level(0, None)?;
tracing::info!("Importing SQLITE data into LMDB: Done.");

View File

@ -1,14 +1,16 @@
use super::Storage;
use crate::error::{Error, ErrorKind};
use lmdb::{Cursor, Transaction};
use nostr_types::Event;
use heed::RwTxn;
use nostr_types::{Event, RelayUrl};
use speedy::Readable;
mod settings;
impl Storage {
const MIGRATION_LEVEL: u32 = 1;
const MAX_MIGRATION_LEVEL: u32 = 3;
pub(super) fn migrate(&self, mut level: u32) -> Result<(), Error> {
if level > Self::MIGRATION_LEVEL {
if level > Self::MAX_MIGRATION_LEVEL {
return Err(ErrorKind::General(format!(
"Migration level {} unknown: This client is older than your data.",
level
@ -16,55 +18,99 @@ impl Storage {
.into());
}
while level < Self::MIGRATION_LEVEL {
let mut txn = self.env.write_txn()?;
while level < Self::MAX_MIGRATION_LEVEL {
self.migrate_inner(level, &mut txn)?;
level += 1;
tracing::info!("LMDB Migration to level {}...", level);
self.migrate_inner(level)?;
self.write_migration_level(level)?;
self.write_migration_level(level, Some(&mut txn))?;
}
txn.commit()?;
Ok(())
}
fn migrate_inner(&self, level: u32) -> Result<(), Error> {
fn migrate_inner<'a>(&'a self, level: u32, txn: &mut RwTxn<'a>) -> Result<(), Error> {
let prefix = format!("LMDB Migration {} -> {}", level, level + 1);
match level {
0 => Ok(()),
1 => self.compute_relationships(),
n => panic!("Unknown migration level {}", n),
}
0 => {
let total = self.get_event_len()? as usize;
tracing::info!(
"{prefix}: Computing and storing event relationships for {total} events..."
);
self.compute_relationships(total, Some(txn))?;
}
1 => {
tracing::info!("{prefix}: Updating Settings...");
self.try_migrate_settings1_settings2(Some(txn))?;
}
2 => {
tracing::info!("{prefix}: Removing invalid relays...");
self.remove_invalid_relays(txn)?;
}
_ => panic!("Unreachable migration level"),
};
tracing::info!("done.");
Ok(())
}
// Load and process every event in order to generate the relationships data
fn compute_relationships(&self) -> Result<(), Error> {
self.disable_sync()?;
fn compute_relationships<'a>(
&'a self,
total: usize,
rw_txn: Option<&mut RwTxn<'a>>,
) -> Result<(), Error> {
let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> {
// track progress
let mut count = 0;
// track progress
let total = self.get_event_stats()?.entries();
let mut count = 0;
let event_txn = self.env.read_txn()?;
for result in self.events.iter(&event_txn)? {
let pair = result?;
let event = Event::read_from_buffer(pair.1)?;
let _ = self.process_relationships_of_event(&event, Some(txn))?;
let txn = self.env.begin_ro_txn()?;
let mut cursor = txn.open_ro_cursor(self.events)?;
let iter = cursor.iter_start();
for result in iter {
match result {
Err(e) => return Err(e.into()),
Ok((_key, val)) => {
let event = Event::read_from_buffer(val)?;
let _ = self.process_relationships_of_event(&event)?;
// track progress
count += 1;
for checkpoint in &[10, 20, 30, 40, 50, 60, 70, 80, 90] {
if count == checkpoint * total / 100 {
tracing::info!("{}% done", checkpoint);
}
}
}
// track progress
count += 1;
if count % 1000 == 0 {
tracing::info!("{}/{}", count, total);
tracing::info!("syncing...");
Ok(())
};
match rw_txn {
Some(txn) => {
f(txn)?;
}
None => {
let mut txn = self.env.write_txn()?;
f(&mut txn)?;
txn.commit()?;
}
};
Ok(())
}
fn remove_invalid_relays<'a>(&'a self, rw_txn: &mut RwTxn<'a>) -> Result<(), Error> {
let bad_relays =
self.filter_relays(|relay| RelayUrl::try_from_str(&relay.url.0).is_err())?;
for relay in &bad_relays {
tracing::info!("Deleting bad relay: {}", relay.url);
self.delete_relay(&relay.url, Some(rw_txn))?;
}
tracing::info!("syncing...");
self.enable_sync()?;
tracing::info!("Deleted {} bad relays", bad_relays.len());
Ok(())
}

View File

@ -0,0 +1,52 @@
// In order for this migration to work in the distant future after all kinds of
// other code has changed, it has to have it's own version of what Settings used
// to look like.
mod settings1;
use settings1::Settings1;
mod settings2;
use settings2::Settings2;
mod theme1;
use crate::error::Error;
use crate::storage::Storage;
use heed::RwTxn;
use speedy::{Readable, Writable};
impl Storage {
pub(in crate::storage) fn try_migrate_settings1_settings2<'a>(
&'a self,
rw_txn: Option<&mut RwTxn<'a>>,
) -> Result<(), Error> {
let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> {
// If something is under the old "settings" key
if let Ok(Some(bytes)) = self.general.get(txn, b"settings") {
let settings1 = Settings1::read_from_buffer(bytes)?;
// Convert it to the new Settings2 structure
let settings2: Settings2 = settings1.into();
let bytes = settings2.write_to_vec()?;
// And store it under the new "settings2" key
self.general.put(txn, b"settings2", &bytes)?;
// Then delete the old "settings" key
self.general.delete(txn, b"settings")?;
}
Ok(())
};
match rw_txn {
Some(txn) => f(txn)?,
None => {
let mut txn = self.env.write_txn()?;
f(&mut txn)?;
txn.commit()?;
}
};
Ok(())
}
}

View File

@ -0,0 +1,77 @@
use super::theme1::{Theme1, ThemeVariant1};
use nostr_types::PublicKey;
use serde::{Deserialize, Serialize};
use speedy::{Readable, Writable};
#[derive(Clone, Debug, Serialize, Deserialize, Readable, Writable)]
pub struct Settings1 {
pub feed_chunk: u64,
pub replies_chunk: u64,
pub overlap: u64,
pub num_relays_per_person: u8,
pub max_relays: u8,
pub public_key: Option<PublicKey>,
pub max_fps: u32,
pub recompute_feed_periodically: bool,
pub feed_recompute_interval_ms: u32,
pub pow: u8,
pub offline: bool,
pub theme: Theme1,
pub set_client_tag: bool,
pub set_user_agent: bool,
pub override_dpi: Option<u32>,
pub reactions: bool,
pub reposts: bool,
pub show_long_form: bool,
pub show_mentions: bool,
pub show_media: bool,
pub load_avatars: bool,
pub load_media: bool,
pub check_nip05: bool,
pub direct_messages: bool,
pub automatically_fetch_metadata: bool,
pub delegatee_tag: String,
pub highlight_unread_events: bool,
pub posting_area_at_top: bool,
pub enable_zap_receipts: bool,
}
impl Default for Settings1 {
fn default() -> Settings1 {
Settings1 {
feed_chunk: 60 * 60 * 12, // 12 hours
replies_chunk: 60 * 60 * 24 * 7, // 1 week
overlap: 300, // 5 minutes
num_relays_per_person: 2,
max_relays: 50,
public_key: None,
max_fps: 12,
recompute_feed_periodically: true,
feed_recompute_interval_ms: 8000,
pow: 0,
offline: false,
theme: Theme1 {
variant: ThemeVariant1::Default,
dark_mode: false,
follow_os_dark_mode: false,
},
set_client_tag: false,
set_user_agent: false,
override_dpi: None,
reactions: true,
reposts: true,
show_long_form: false,
show_mentions: true,
show_media: true,
load_avatars: true,
load_media: true,
check_nip05: true,
direct_messages: true,
automatically_fetch_metadata: true,
delegatee_tag: String::new(),
highlight_unread_events: true,
posting_area_at_top: true,
enable_zap_receipts: true,
}
}
}

View File

@ -0,0 +1,229 @@
use super::settings1::Settings1;
use super::theme1::{Theme1, ThemeVariant1};
use nostr_types::PublicKey;
use serde::{Deserialize, Serialize};
use speedy::{Readable, Writable};
#[derive(Clone, Debug, Serialize, Deserialize, Readable, Writable)]
pub struct Settings2 {
// ID settings
pub public_key: Option<PublicKey>,
pub log_n: u8,
// Network settings
pub offline: bool,
pub load_avatars: bool,
pub load_media: bool,
pub check_nip05: bool,
pub automatically_fetch_metadata: bool,
// Relay settings
pub num_relays_per_person: u8,
pub max_relays: u8,
// Feed Settings
pub feed_chunk: u64,
pub replies_chunk: u64,
pub person_feed_chunk: u64,
pub overlap: u64,
// Event Selection
pub reposts: bool,
pub show_long_form: bool,
pub show_mentions: bool,
pub direct_messages: bool,
pub future_allowance_secs: u64,
// Event Content Settings
pub reactions: bool,
pub enable_zap_receipts: bool,
pub show_media: bool,
// Posting Settings
pub pow: u8,
pub set_client_tag: bool,
pub set_user_agent: bool,
pub delegatee_tag: String,
// UI settings
pub max_fps: u32,
pub recompute_feed_periodically: bool,
pub feed_recompute_interval_ms: u32,
pub theme: Theme1,
pub override_dpi: Option<u32>,
pub highlight_unread_events: bool,
pub posting_area_at_top: bool,
pub status_bar: bool,
pub image_resize_algorithm: String,
// Staletime settings
pub relay_list_becomes_stale_hours: u64,
pub metadata_becomes_stale_hours: u64,
pub nip05_becomes_stale_if_valid_hours: u64,
pub nip05_becomes_stale_if_invalid_minutes: u64,
pub avatar_becomes_stale_hours: u64,
pub media_becomes_stale_hours: u64,
// Websocket settings
pub max_websocket_message_size_kb: usize,
pub max_websocket_frame_size_kb: usize,
pub websocket_accept_unmasked_frames: bool,
pub websocket_connect_timeout_sec: u64,
pub websocket_ping_frequency_sec: u64,
// HTTP settings
pub fetcher_metadata_looptime_ms: u64,
pub fetcher_looptime_ms: u64,
pub fetcher_connect_timeout_sec: u64,
pub fetcher_timeout_sec: u64,
pub fetcher_max_requests_per_host: usize,
pub fetcher_host_exclusion_on_low_error_secs: u64,
pub fetcher_host_exclusion_on_med_error_secs: u64,
pub fetcher_host_exclusion_on_high_error_secs: u64,
pub nip11_lines_to_output_on_error: usize,
// Database settings
pub prune_period_days: u64,
}
impl Default for Settings2 {
fn default() -> Settings2 {
Settings2 {
// ID settings
public_key: None,
log_n: 18,
// Network settings
offline: false,
load_avatars: true,
load_media: true,
check_nip05: true,
automatically_fetch_metadata: true,
// Relay settings
num_relays_per_person: 2,
max_relays: 50,
// Feed settings
feed_chunk: 60 * 60 * 12, // 12 hours
replies_chunk: 60 * 60 * 24 * 7, // 1 week
person_feed_chunk: 60 * 60 * 24 * 30, // 1 month
overlap: 300, // 5 minutes
// Event Selection
reposts: true,
show_long_form: false,
show_mentions: true,
direct_messages: true,
future_allowance_secs: 60 * 15, // 15 minutes
// Event Content Settings
reactions: true,
enable_zap_receipts: true,
show_media: true,
// Posting settings
pow: 0,
set_client_tag: false,
set_user_agent: false,
delegatee_tag: String::new(),
// UI settings
max_fps: 12,
recompute_feed_periodically: true,
feed_recompute_interval_ms: 8000,
theme: Theme1 {
variant: ThemeVariant1::Default,
dark_mode: false,
follow_os_dark_mode: false,
},
override_dpi: None,
highlight_unread_events: true,
posting_area_at_top: true,
status_bar: false,
image_resize_algorithm: "CatmullRom".to_owned(),
// Staletime settings
relay_list_becomes_stale_hours: 8,
metadata_becomes_stale_hours: 8,
nip05_becomes_stale_if_valid_hours: 8,
nip05_becomes_stale_if_invalid_minutes: 30, // 30 minutes
avatar_becomes_stale_hours: 8,
media_becomes_stale_hours: 8,
// Websocket settings
max_websocket_message_size_kb: 1024, // 1 MB
max_websocket_frame_size_kb: 1024, // 1 MB
websocket_accept_unmasked_frames: false,
websocket_connect_timeout_sec: 15,
websocket_ping_frequency_sec: 55,
// HTTP settings
fetcher_metadata_looptime_ms: 3000,
fetcher_looptime_ms: 1800,
fetcher_connect_timeout_sec: 15,
fetcher_timeout_sec: 30,
fetcher_max_requests_per_host: 3,
fetcher_host_exclusion_on_low_error_secs: 30,
fetcher_host_exclusion_on_med_error_secs: 60,
fetcher_host_exclusion_on_high_error_secs: 600,
nip11_lines_to_output_on_error: 10,
// Database settings
prune_period_days: 30,
}
}
}
impl From<Settings1> for Settings2 {
fn from(old: Settings1) -> Settings2 {
Settings2 {
// ID settings
public_key: old.public_key,
// Network settings
offline: old.offline,
load_avatars: old.load_avatars,
load_media: old.load_media,
check_nip05: old.check_nip05,
automatically_fetch_metadata: old.automatically_fetch_metadata,
// Relay settings
num_relays_per_person: old.num_relays_per_person,
max_relays: old.max_relays,
// Feed settings
feed_chunk: old.feed_chunk,
replies_chunk: old.replies_chunk,
overlap: old.overlap,
// Event Selection
reposts: old.reposts,
show_long_form: old.show_long_form,
show_mentions: old.show_mentions,
direct_messages: old.direct_messages,
// Event Content Settings
reactions: old.reactions,
enable_zap_receipts: old.enable_zap_receipts,
show_media: old.show_media,
// Posting settings
pow: old.pow,
set_client_tag: old.set_client_tag,
set_user_agent: old.set_user_agent,
delegatee_tag: old.delegatee_tag,
// UI settings
max_fps: old.max_fps,
recompute_feed_periodically: old.recompute_feed_periodically,
feed_recompute_interval_ms: old.feed_recompute_interval_ms,
theme: old.theme,
override_dpi: old.override_dpi,
highlight_unread_events: old.highlight_unread_events,
posting_area_at_top: old.posting_area_at_top,
..Default::default()
}
}
}

View File

@ -0,0 +1,17 @@
use serde::{Deserialize, Serialize};
use speedy::{Readable, Writable};
// note: if we store anything inside the variants, we can't use macro_rules.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Readable, Writable)]
pub enum ThemeVariant1 {
Classic,
Default,
Roundy,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Readable, Writable)]
pub struct Theme1 {
pub variant: ThemeVariant1,
pub dark_mode: bool,
pub follow_os_dark_mode: bool,
}

File diff suppressed because it is too large Load Diff

View File

@ -84,7 +84,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
render_a_feed(app, ctx, frame, ui, feed, false, id);
}
FeedKind::Inbox(indirect) => {
if GLOBALS.signer.public_key().is_none() {
if app.settings.public_key.is_none() {
ui.horizontal_wrapped(|ui| {
ui.label("You need to ");
if ui.link("setup an identity").clicked() {
@ -216,19 +216,6 @@ fn render_note_maybe_fake(
is_last,
} = feed_note_params;
// We always get the event even offscreen so we can estimate its height
let event = match GLOBALS.storage.read_event(id) {
Ok(Some(event)) => event,
_ => return,
};
// Stop rendering if the note is included in a collapsed thread
if let Some((id, _)) = event.replies_to() {
if app.collapsed.contains(&id) {
return;
}
}
let screen_rect = ctx.input(|i| i.screen_rect); // Rect
let pos2 = ui.next_widget_position();
@ -265,8 +252,8 @@ fn render_note_maybe_fake(
ui.add_space(height);
// Yes, and we need to fake render threads to get their approx height too.
if threaded && !as_reply_to {
let replies = GLOBALS.storage.get_replies(event.id).unwrap_or(vec![]);
if threaded && !as_reply_to && !app.collapsed.contains(&id) {
let replies = GLOBALS.storage.get_replies(id).unwrap_or(vec![]);
let iter = replies.iter();
let first = replies.first();
let last = replies.last();

View File

@ -7,7 +7,7 @@ use eframe::{
epaint::Vec2,
};
use egui::{RichText, Ui};
use nostr_types::{ContentSegment, Id, IdHex, NostrBech32, PublicKey, Span, Tag, Url};
use nostr_types::{ContentSegment, EventAddr, Id, IdHex, NostrBech32, PublicKey, Span, Tag, Url};
use std::{
cell::{Ref, RefCell},
rc::Rc,
@ -30,11 +30,7 @@ pub(super) fn render_content(
ContentSegment::NostrUrl(nurl) => {
match &nurl.0 {
NostrBech32::EventAddr(ea) => {
// FIXME - we should link to the event instead
ui.label(
RichText::new(format!("nostr:{}", ea.as_bech32_string()))
.underline(),
);
render_parameterized_event_link(app, ui, note.event.id, ea);
}
NostrBech32::EventPointer(ep) => {
let mut render_link = true;
@ -221,6 +217,32 @@ pub(super) fn render_event_link(
};
}
pub(super) fn render_parameterized_event_link(
app: &mut GossipUi,
ui: &mut Ui,
referenced_by_id: Id,
event_addr: &EventAddr,
) {
let nam = format!("nostr:{}", event_addr.as_bech32_string());
if ui.link(&nam).clicked() {
if let Ok(Some(prevent)) = GLOBALS
.storage
.get_parameterized_replaceable_event(event_addr)
{
app.set_page(Page::Feed(FeedKind::Thread {
id: prevent.id,
referenced_by: referenced_by_id,
author: Some(prevent.pubkey),
}));
} else {
GLOBALS
.status_queue
.write()
.write("Parameterized event not found.".to_owned());
}
};
}
pub(super) fn render_hashtag(ui: &mut Ui, s: &String) {
if ui.link(format!("#{}", s)).clicked() {
GLOBALS

View File

@ -137,8 +137,9 @@ pub(super) fn render_note(
app.height.insert(id, bottom.y - top.y);
// Mark post as viewed if hovered AND we are not scrolling
if inner_response.response.hovered() && app.current_scroll_offset == 0.0 {
let _ = GLOBALS.storage.mark_event_viewed(id);
if !viewed && inner_response.response.hovered() && app.current_scroll_offset == 0.0
{
let _ = GLOBALS.storage.mark_event_viewed(id, None);
}
// Record if the rendered note was visible
@ -158,7 +159,7 @@ pub(super) fn render_note(
}
// even if muted, continue rendering thread children
if threaded && !as_reply_to {
if threaded && !as_reply_to && !app.collapsed.contains(&id) {
let replies = GLOBALS.storage.get_replies(id).unwrap_or(vec![]);
let iter = replies.iter();
let first = replies.first();
@ -363,6 +364,26 @@ fn render_note_inner(
let nostr_url: NostrUrl = event_pointer.into();
ui.output_mut(|o| o.copied_text = format!("{}", nostr_url));
}
if ui.button("Copy web link").clicked() {
let event_pointer = EventPointer {
id: note.event.id,
relays: match GLOBALS.storage.get_event_seen_on_relay(note.event.id)
{
Ok(vec) => {
vec.iter().map(|(url, _)| url.to_unchecked_url()).collect()
}
Err(_) => vec![],
},
author: None,
kind: None,
};
ui.output_mut(|o| {
o.copied_text = format!(
"https://nostr.com/{}",
event_pointer.as_bech32_string()
)
});
}
if ui.button("Copy note1 Id").clicked() {
let nostr_url: NostrUrl = note.event.id.into();
ui.output_mut(|o| o.copied_text = format!("{}", nostr_url));
@ -378,7 +399,7 @@ fn render_note_inner(
if ui.button("Dismiss").clicked() {
GLOBALS.dismissed.blocking_write().push(note.event.id);
}
if Some(note.event.pubkey) == GLOBALS.signer.public_key()
if Some(note.event.pubkey) == app.settings.public_key
&& note.deletion.is_none()
{
if ui.button("Delete").clicked() {
@ -522,6 +543,26 @@ fn render_note_inner(
});
}
// proxied?
if let Some((proxy, id)) = note.event.proxy() {
Frame::none()
.inner_margin(Margin {
left: footer_margin_left,
bottom: 0.0,
right: 0.0,
top: 8.0,
})
.show(ui, |ui| {
let color = app.settings.theme.warning_marker_text_color();
ui.horizontal_wrapped(|ui| {
ui.add(Label::new(
RichText::new(format!("proxied from {}: ", proxy)).color(color),
));
crate::ui::widgets::break_anywhere_hyperlink_to(ui, id, id);
});
});
}
// Footer
if !hide_footer {
Frame::none()
@ -549,6 +590,16 @@ fn render_note_inner(
o.copied_text =
serde_json::to_string(&note.event).unwrap()
});
} else if note.event.kind == EventKind::EncryptedDirectMessage {
ui.output_mut(|o| {
if let Ok(m) =
GLOBALS.signer.decrypt_message(&note.event)
{
o.copied_text = m
} else {
o.copied_text = note.event.content.clone()
}
});
} else {
ui.output_mut(|o| {
o.copied_text = note.event.content.clone()

View File

@ -92,19 +92,24 @@ impl NoteData {
// Compute the content to our needs
let display_content = match event.kind {
EventKind::TextNote => event.content.trim().to_string(),
EventKind::Repost => {
if !event.content.trim().is_empty() && embedded_event.is_none() {
"REPOSTED EVENT IS NOT RELEVANT".to_owned()
} else {
"".to_owned()
}
}
EventKind::Repost => "".to_owned(),
EventKind::EncryptedDirectMessage => match GLOBALS.signer.decrypt_message(&event) {
Ok(m) => m,
Err(_) => "DECRYPTION FAILED".to_owned(),
},
EventKind::LongFormContent => event.content.clone(),
_ => "NON FEED RELATED EVENT".to_owned(),
_ => {
let mut dc = "UNSUPPORTED EVENT KIND".to_owned();
// support the 'alt' tag of NIP-31:
for tag in &event.tags {
if let Tag::Other { tag, data } = tag {
if tag == "alt" && !data.is_empty() {
dc = format!("UNSUPPORTED EVENT KIND, ALT: {}", data[0]);
}
}
}
dc
}
};
// shatter content here so we can use it in our content analysis

View File

@ -52,162 +52,79 @@ pub(super) fn update(_app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::F
ui.separator();
ui.add_space(6.0);
let general_stats = GLOBALS
.storage
.get_general_stats()
.map(|s| format!("General: {} records, {} pages", s.entries(), s.leaf_pages()))
.unwrap_or("".to_owned());
ui.label(general_stats);
ui.label(format!(
"General: {} records",
GLOBALS.storage.get_general_len().unwrap_or(0)
));
ui.add_space(6.0);
let event_stats = GLOBALS
.storage
.get_event_stats()
.map(|s| format!("Events: {} records, {} pages", s.entries(), s.leaf_pages()))
.unwrap_or("".to_owned());
ui.label(event_stats);
ui.label(format!(
"Events: {} records",
GLOBALS.storage.get_event_len().unwrap_or(0)
));
ui.add_space(6.0);
let event_ek_pk_index_stats = GLOBALS
.storage
.get_event_ek_pk_index_stats()
.map(|s| {
format!(
"Event Index (EK-PK): {} records, {} pages",
s.entries(),
s.leaf_pages()
)
})
.unwrap_or("".to_owned());
ui.label(event_ek_pk_index_stats);
ui.label(format!(
"Event Index (EK-PK): {} records",
GLOBALS.storage.get_event_ek_pk_index_len().unwrap_or(0)
));
ui.add_space(6.0);
let event_ek_c_index_stats = GLOBALS
.storage
.get_event_ek_c_index_stats()
.map(|s| {
format!(
"Event Index (EK-C): {} records, {} pages",
s.entries(),
s.leaf_pages()
)
})
.unwrap_or("".to_owned());
ui.label(event_ek_c_index_stats);
ui.label(format!(
"Event Index (EK-C): {} records",
GLOBALS.storage.get_event_ek_c_index_len().unwrap_or(0)
));
ui.add_space(6.0);
let event_references_person_stats = GLOBALS
.storage
.get_event_references_person_stats()
.map(|s| {
format!(
"Event Index (References Person): {} records, {} pages",
s.entries(),
s.leaf_pages()
)
})
.unwrap_or("".to_owned());
ui.label(event_references_person_stats);
ui.label(format!(
"Event Index (References Person): {} records",
GLOBALS
.storage
.get_event_references_person_len()
.unwrap_or(0)
));
ui.add_space(6.0);
let event_tags_stats = GLOBALS
.storage
.get_event_tags_stats()
.map(|s| {
format!(
"Event Index (Tags): {} records, {} pages",
s.entries(),
s.leaf_pages()
)
})
.unwrap_or("".to_owned());
ui.label(event_tags_stats);
ui.label(format!(
"Event Relationships: {} records",
GLOBALS.storage.get_relationships_len().unwrap_or(0)
));
ui.add_space(6.0);
let relationships_stats = GLOBALS
.storage
.get_relationships_stats()
.map(|s| {
format!(
"Event Relationships: {} records, {} pages",
s.entries(),
s.leaf_pages()
)
})
.unwrap_or("".to_owned());
ui.label(relationships_stats);
ui.label(format!(
"Event Seen on Relay: {} records",
GLOBALS.storage.get_event_seen_on_relay_len().unwrap_or(0)
));
ui.add_space(6.0);
let event_seen_on_relay_stats = GLOBALS
.storage
.get_event_seen_on_relay_stats()
.map(|s| {
format!(
"Event Seen-on Relay: {} records, {} pages",
s.entries(),
s.leaf_pages()
)
})
.unwrap_or("".to_owned());
ui.label(event_seen_on_relay_stats);
ui.label(format!(
"Event Viewed: {} records",
GLOBALS.storage.get_event_viewed_len().unwrap_or(0)
));
ui.add_space(6.0);
let event_viewed_stats = GLOBALS
.storage
.get_event_viewed_stats()
.map(|s| {
format!(
"Event Viewed: {} records, {} pages",
s.entries(),
s.leaf_pages()
)
})
.unwrap_or("".to_owned());
ui.label(event_viewed_stats);
ui.label(format!(
"Hashtags: {} records",
GLOBALS.storage.get_hashtags_len().unwrap_or(0)
));
ui.add_space(6.0);
let hashtags_stats = GLOBALS
.storage
.get_hashtags_stats()
.map(|s| {
format!(
"Hashtags: {} records, {} pages",
s.entries(),
s.leaf_pages()
)
})
.unwrap_or("".to_owned());
ui.label(hashtags_stats);
ui.label(format!(
"Relays: {} records",
GLOBALS.storage.get_relays_len().unwrap_or(0)
));
ui.add_space(6.0);
let relays_stats = GLOBALS
.storage
.get_relays_stats()
.map(|s| format!("Relays: {} records, {} pages", s.entries(), s.leaf_pages()))
.unwrap_or("".to_owned());
ui.label(relays_stats);
ui.label(format!(
"People: {} records",
GLOBALS.storage.get_people_len().unwrap_or(0)
));
ui.add_space(6.0);
let people_stats = GLOBALS
.storage
.get_people_stats()
.map(|s| format!("People: {} records, {} pages", s.entries(), s.leaf_pages()))
.unwrap_or("".to_owned());
ui.label(people_stats);
ui.add_space(6.0);
let person_relays_stats = GLOBALS
.storage
.get_person_relays_stats()
.map(|s| {
format!(
"Person-Relays: {} records, {} pages",
s.entries(),
s.leaf_pages()
)
})
.unwrap_or("".to_owned());
ui.label(person_relays_stats);
ui.label(format!(
"Person-Relays: {} records",
GLOBALS.storage.get_person_relays_len().unwrap_or(0)
));
ui.add_space(6.0);
});
}

View File

@ -135,6 +135,16 @@ struct SubMenuState {
submenu_states: HashMap<SubMenu, bool>,
}
#[derive(Eq, Hash, PartialEq)]
enum SettingsTab {
Content,
Database,
Id,
Network,
Posting,
Ui,
}
impl SubMenuState {
fn new() -> Self {
let mut submenu_states: HashMap<SubMenu, bool> = HashMap::new();
@ -169,6 +179,7 @@ struct GossipUi {
next_frame: Instant,
override_dpi: bool,
override_dpi_value: u32,
original_dpi_value: u32,
current_scroll_offset: f32,
future_scroll_offset: f32,
@ -199,6 +210,7 @@ struct GossipUi {
inbox_include_indirect: bool,
submenu_ids: HashMap<SubMenu, egui::Id>,
submenu_state: SubMenuState,
settings_tab: SettingsTab,
// General Data
about: About,
@ -397,6 +409,7 @@ impl GossipUi {
next_frame: Instant::now(),
override_dpi,
override_dpi_value,
original_dpi_value: override_dpi_value,
current_scroll_offset: 0.0,
future_scroll_offset: 0.0,
qr_codes: HashMap::new(),
@ -420,6 +433,7 @@ impl GossipUi {
.unwrap_or(false),
submenu_ids,
submenu_state: SubMenuState::new(),
settings_tab: SettingsTab::Id,
about: crate::about::about(),
icon: icon_texture_handle,
placeholder_avatar: placeholder_avatar_texture_handle,
@ -559,6 +573,23 @@ impl eframe::App for GossipUi {
ctx.input(|i| {
requested_scroll = i.scroll_delta.y;
});
// use keys to scroll
ctx.input(|i| {
if i.key_pressed(egui::Key::ArrowDown) {
requested_scroll -= 30.0;
}
if i.key_pressed(egui::Key::ArrowUp) {
requested_scroll = 30.0;
}
if i.key_pressed(egui::Key::PageUp) {
requested_scroll = 150.0;
}
if i.key_pressed(egui::Key::PageDown) {
requested_scroll -= 150.0;
}
});
self.future_scroll_offset += requested_scroll;
// Move by 10% of future scroll offsets
@ -586,6 +617,54 @@ impl eframe::App for GossipUi {
relays::entry_dialog(ctx, self);
}
if self.settings.status_bar {
egui::TopBottomPanel::top("stats-bar")
.frame(
egui::Frame::side_top_panel(&self.settings.theme.get_style()).inner_margin(
egui::Margin {
left: 0.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
},
),
)
.show(
ctx,
|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| {
let in_flight = GLOBALS.fetcher.requests_in_flight();
let queued = GLOBALS.fetcher.requests_queued();
let events = GLOBALS
.storage
.get_event_len()
.unwrap_or(0);
let relays = GLOBALS.connected_relays.len();
let processed = GLOBALS.events_processed.load(Ordering::Relaxed);
let subs = GLOBALS.open_subscriptions.load(Ordering::Relaxed);
let stats_message = format!(
"EVENTS PROCESSED={} STORED={} RELAYS CONNS={} SUBS={} HTTP: {} / {}",
processed,
events,
relays,
subs,
in_flight,
in_flight + queued
);
let stats_message = RichText::new(stats_message)
.color(self.settings.theme.notice_marker_text_color());
ui.add(Label::new(stats_message))
.on_hover_text(
"events processed: number of events relays have sent to us, including duplicates.\n\
events stored: number of unique events in storage\n\
relay conns: number of relays currently connected\n\
relay subs: number of subscriptions that have not come to EOSE yet\n\
http: number of fetches in flight / number of requests queued");
});
},
);
}
egui::SidePanel::left("main-naviation-panel")
.show_separator_line(false)
.frame(
@ -820,8 +899,7 @@ impl eframe::App for GossipUi {
},
);
//let show_status = self.show_post_area && !self.settings.posting_area_at_top;
let show_status = true;
let show_status = self.show_post_area && !self.settings.posting_area_at_top;
let resizable = true;
@ -833,7 +911,7 @@ impl eframe::App for GossipUi {
left: 20.0,
right: 18.0,
top: 10.0,
bottom: 0.0,
bottom: 10.0,
}
} else {
egui::Margin {
@ -852,29 +930,6 @@ impl eframe::App for GossipUi {
ui.add_space(7.0);
feed::post::posting_area(self, ctx, frame, ui);
}
ui.vertical(|ui| {
ui.add_space(5.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| {
let in_flight = GLOBALS.fetcher.requests_in_flight();
let queued = GLOBALS.fetcher.requests_queued();
let events = GLOBALS
.storage
.get_event_stats()
.map(|s| s.entries())
.unwrap_or(0);
let relays = GLOBALS.connected_relays.len();
let stats_message = format!(
"EVENTS: {} RELAYS CONNECTED: {} HTTP: {} / {}",
events,
relays,
in_flight,
in_flight + queued
);
let stats_message = RichText::new(stats_message)
.color(self.settings.theme.notice_marker_text_color());
ui.add(Label::new(stats_message));
});
});
});
// Prepare local zap data once per frame for easier compute at render time
@ -981,8 +1036,10 @@ impl GossipUi {
.to_overlord
.send(ToOverlordMessage::UpdateMetadata(person.pubkey));
}
if ui.button("View Their Posts").clicked() {
app.set_page(Page::Feed(FeedKind::Person(person.pubkey)));
if app.page != Page::Feed(FeedKind::Person(person.pubkey)) {
if ui.button("View Their Posts").clicked() {
app.set_page(Page::Feed(FeedKind::Person(person.pubkey)));
}
}
});

View File

@ -8,10 +8,11 @@ use egui::{Context, Image, RichText, ScrollArea, Sense, Ui, Vec2};
use std::sync::atomic::Ordering;
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
let people: Vec<Person> = match GLOBALS.storage.filter_people(|p| p.followed) {
let mut people: Vec<Person> = match GLOBALS.storage.filter_people(|p| p.followed) {
Ok(people) => people,
Err(_) => return,
};
people.sort_unstable();
ui.add_space(12.0);

View File

@ -108,12 +108,17 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut Frame, ui:
}
});
let summary = event
let mut summary = event
.content
.get(0..event.content.len().min(100))
.unwrap_or("...")
.replace('\n', " ");
if summary.is_empty() {
// Show something they can click on anyways
summary = "[no event summary]".to_owned();
}
if ui.add(Label::new(summary).sense(Sense::click())).clicked() {
app.set_page(Page::Feed(FeedKind::Thread {
id: event.id,

View File

@ -1,326 +0,0 @@
use super::{GossipUi, ThemeVariant};
use crate::comms::ToOverlordMessage;
use crate::GLOBALS;
use eframe::egui;
use egui::widgets::{Button, Slider};
use egui::{Align, Context, Layout, ScrollArea, Ui, Vec2};
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.heading("Settings");
ui.with_layout(Layout::bottom_up(Align::Center), |ui| {
ui.separator();
ui.add_space(12.0);
if ui.button("SAVE CHANGES").clicked() {
// Copy local settings to global settings
*GLOBALS.settings.write() = app.settings.clone();
// Tell the overlord to save them
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::SaveSettings);
}
ui.with_layout(Layout::top_down(Align::Min), |ui| {
ui.add_space(10.0);
ui.separator();
ScrollArea::vertical()
.id_source("settings")
.override_scroll_delta(Vec2 { x: 0.0, y: app.current_scroll_offset })
.show(ui, |ui| {
ui.add_space(12.0);
ui.heading("How Many Relays to Query");
ui.horizontal(|ui| {
ui.label("Number of relays to query per person: ").on_hover_text("We will query N relays per person. Many people share the same relays so those will be queried about multiple people. Takes affect on restart. I recommend 2. Too many and gossip will (currently) keep connecting to new relays trying to find the unfindable, loading many events from each. Takes effect on restart.");
ui.add(Slider::new(&mut app.settings.num_relays_per_person, 1..=4).text("relays"));
});
ui.horizontal(|ui| {
ui.label("Maximum following feed relays: ")
.on_hover_text(
"We will not stay connected to more than this many relays for following feed. Takes affect on restart. During these early days of nostr, I recommend capping this at around 20 to 30.",
);
ui.add(Slider::new(&mut app.settings.max_relays, 5..=100).text("relays"));
});
ui.add_space(12.0);
ui.separator();
ui.add_space(12.0);
ui.heading("How Many Posts to Load");
ui.horizontal(|ui| {
ui.label("Feed Chunk: ").on_hover_text("This is the amount of time backwards from now that we will load events from. You'll eventually be able to load more, one chunk at a time. Mostly takes effect on restart.");
ui.add(Slider::new(&mut app.settings.feed_chunk, 600..=86400).text("seconds, "));
ui.label(secs_to_string(app.settings.feed_chunk));
});
ui.horizontal(|ui| {
ui.label("Replies Chunk: ").on_hover_text("This is the amount of time backwards from now that we will load replies, mentions, and DMs from. You'll eventually be able to load more, one chunk at a time. Mostly takes effect on restart.");
ui.add(Slider::new(&mut app.settings.replies_chunk, 86400..=2592000).text("seconds, "));
ui.label(secs_to_string(app.settings.replies_chunk));
});
ui.horizontal(|ui| {
ui.label("Overlap: ").on_hover_text("If we recently loaded events up to time T, but restarted, we will now load events starting from time T minus overlap. Takes effect on restart. I recommend 300 (5 minutes).");
ui.add(Slider::new(&mut app.settings.overlap, 0..=3600).text("seconds, "));
ui.label(secs_to_string(app.settings.overlap));
});
ui.add_space(12.0);
ui.separator();
ui.add_space(12.0);
ui.heading("Feed");
ui.checkbox(
&mut app.settings.recompute_feed_periodically,
"Recompute feed periodically. If this is off, you will get a refresh button"
);
ui.horizontal(|ui| {
ui.label("Recompute feed every (milliseconds): ")
.on_hover_text(
"The UI redraws frequently. We recompute the feed less frequently to conserve CPU. Takes effect when the feed next recomputes. I recommend 3500.",
);
ui.add(Slider::new(&mut app.settings.feed_recompute_interval_ms, 1000..=12000).text("milliseconds"));
});
ui.add_space(12.0);
ui.separator();
ui.add_space(12.0);
ui.heading("What Posts to Include");
ui.checkbox(
&mut app.settings.reactions,
"Enable reactions (show and react)",
);
ui.checkbox(
&mut app.settings.enable_zap_receipts,
"Enable zap receipts",
);
ui.checkbox(
&mut app.settings.reposts,
"Enable reposts (show)",
);
ui.checkbox(
&mut app.settings.direct_messages,
"Show Direct Messages",
)
.on_hover_text("Takes effect fully only on restart.");
ui.checkbox(
&mut app.settings.show_long_form,
"Show Long-Form Posts",
)
.on_hover_text("Takes effect fully only on restart.");
ui.add_space(12.0);
ui.separator();
ui.add_space(12.0);
ui.heading("Post look-and-feel");
ui.checkbox(
&mut app.settings.show_mentions,
"Render mentions inline",
)
.on_hover_text(if app.settings.show_mentions {
"Disable to just show a link to a mentioned post where it appears in the text"
} else {
"Enable to render a mentioned post where it appears in the text"
});
ui.checkbox(
&mut app.settings.show_media,
"Render all media inline automatically",
)
.on_hover_text(
"If off, you have to click to (potentially fetch and) render media inline. If on, all media referenced by posts in your feed will be (potentially fetched and) rendered. However, if Fetch Media is disabled, only cached media can be shown as media will not be fetched."
);
ui.add_space(12.0);
ui.separator();
ui.add_space(12.0);
ui.heading("Posting");
ui.horizontal(|ui| {
ui.label("Proof of Work: ")
.on_hover_text("The larger the number, the longer it takes.");
ui.add(Slider::new(&mut app.settings.pow, 0..=40).text("leading zero bits"));
});
ui.add_space(12.0);
ui.checkbox(
&mut app.settings.set_client_tag,
"Add tag [\"client\",\"gossip\"] to posts",
)
.on_hover_text("Takes effect immediately.");
ui.add_space(12.0);
ui.separator();
ui.add_space(12.0);
ui.heading("Network");
ui.checkbox(&mut app.settings.offline, "Offline Mode")
.on_hover_text("If selected, no network requests will be issued. Takes effect on restart.");
ui.checkbox(&mut app.settings.automatically_fetch_metadata, "Automatically Fetch Metadata")
.on_hover_text("If enabled, metadata that is entirely missing will be fetched as you scroll past people. Existing metadata won't be updated. Takes effect on save.");
ui.checkbox(&mut app.settings.load_avatars, "Fetch Avatars")
.on_hover_text("If disabled, avatars will not be fetched, but cached avatars will still display. Takes effect on save.");
ui.checkbox(&mut app.settings.load_media, "Fetch Media")
.on_hover_text("If disabled, no new media will be fetched, but cached media will still display. Takes effect on save.");
ui.checkbox(&mut app.settings.check_nip05, "Check NIP-05")
.on_hover_text("If disabled, NIP-05 fetches will not be performed, but existing knowledge will be preserved, and following someone by NIP-05 will override this and do the fetch. Takes effect on save.");
ui.add_space(12.0);
ui.checkbox(
&mut app.settings.set_user_agent,
&format!("Send User-Agent Header to Relays: gossip/{}", app.about.version),
)
.on_hover_text("Takes effect on next relay connection.");
ui.add_space(12.0);
ui.separator();
ui.add_space(12.0);
ui.heading("User Interface");
ui.add_space(12.0);
ui.checkbox(
&mut app.settings.highlight_unread_events,
"Highlight unread events",
);
ui.checkbox(
&mut app.settings.posting_area_at_top,
"Show posting area at the top instead of the bottom",
);
ui.add_space(12.0);
ui.horizontal(|ui| {
ui.label("Theme:");
if !app.settings.theme.follow_os_dark_mode {
if app.settings.theme.dark_mode {
if ui
.add(Button::new("🌙 Dark"))
.on_hover_text("Switch to light mode")
.clicked()
{
app.settings.theme.dark_mode = false;
super::theme::apply_theme(app.settings.theme, ctx);
}
} else {
if ui
.add(Button::new("☀ Light"))
.on_hover_text("Switch to dark mode")
.clicked()
{
app.settings.theme.dark_mode = true;
super::theme::apply_theme(app.settings.theme, ctx);
}
}
}
let theme_combo = egui::ComboBox::from_id_source("Theme");
theme_combo
.selected_text( app.settings.theme.name() )
.show_ui(ui, |ui| {
for theme_variant in ThemeVariant::all() {
if ui.add(egui::widgets::SelectableLabel::new(*theme_variant == app.settings.theme.variant, theme_variant.name())).clicked() {
app.settings.theme.variant = *theme_variant;
super::theme::apply_theme(app.settings.theme, ctx);
};
}
});
ui.checkbox(&mut app.settings.theme.follow_os_dark_mode,"Follow OS dark-mode")
.on_hover_text("Follow the operating system setting for dark-mode (requires app-restart to take effect)");
});
ui.add_space(12.0);
ui.horizontal(|ui| {
ui.label("Override DPI: ")
.on_hover_text(
"On some systems, DPI is not reported properly. In other cases, people like to zoom in or out. This lets you.",
);
ui.checkbox(
&mut app.override_dpi,
"Override to ");
ui.add(Slider::new(&mut app.override_dpi_value, 72..=250).text("DPI"));
if ui.button("Try it now").clicked() {
let ppt: f32 = app.override_dpi_value as f32 / 72.0;
ctx.set_pixels_per_point(ppt);
}
// transfer to app.settings
app.settings.override_dpi = if app.override_dpi {
// Set it in settings to be saved on button press
Some(app.override_dpi_value)
} else {
None
};
});
ui.add_space(12.0);
ui.horizontal(|ui| {
ui.label("Maximum FPS: ")
.on_hover_text(
"The UI redraws every frame. By limiting the maximum FPS you can reduce load on your CPU. Takes effect immediately. I recommend 10, maybe even less.",
);
ui.add(Slider::new(&mut app.settings.max_fps, 2..=60).text("Frames per second"));
});
ui.add_space(12.0);
ui.separator();
ui.add_space(12.0);
if ui.button("Prune Database")
.on_hover_text("This will delete events older than six months, but the LMDB files will continue consuming disk space. To compact them, copy withem with `mdb_copy -c` when gossip is not running.")
.clicked() {
GLOBALS.status_queue.write().write(
"Pruning database, please wait (this takes a long time)...".to_owned()
);
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PruneDatabase);
}
ui.add_space(12.0);
});
});
});
}
fn secs_to_string(secs: u64) -> String {
let days = secs / 86400;
let remainder = secs % 86400;
let hours = remainder / 3600;
let remainder = remainder % 3600;
let minutes = remainder / 60;
let seconds = remainder % 60;
let mut output: String = String::new();
if days > 0 {
output.push_str(&format!(" {} days", days));
}
if hours > 0 {
output.push_str(&format!(" {} hours", hours));
}
if minutes > 0 {
output.push_str(&format!(" {} minutes", minutes));
}
output.push_str(&format!(" {} seconds", seconds));
output
}

102
src/ui/settings/content.rs Normal file
View File

@ -0,0 +1,102 @@
use crate::ui::GossipUi;
use eframe::egui;
use egui::widgets::Slider;
use egui::{Context, Ui};
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.heading("Content");
ui.add_space(10.0);
ui.heading("Feed Settings");
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.label("Feed Chunk: ").on_hover_text("This is the amount of time backwards from now that we will load events from. You'll eventually be able to load more, one chunk at a time. Mostly takes effect on restart.");
ui.add(Slider::new(&mut app.settings.feed_chunk, 600..=86400).text("seconds, "));
ui.label(secs_to_string(app.settings.feed_chunk));
});
ui.horizontal(|ui| {
ui.label("Replies Chunk: ").on_hover_text("This is the amount of time backwards from now that we will load replies, mentions, and DMs from. You'll eventually be able to load more, one chunk at a time. Mostly takes effect on restart.");
ui.add(Slider::new(&mut app.settings.replies_chunk, 86400..=2592000).text("seconds, "));
ui.label(secs_to_string(app.settings.replies_chunk));
});
ui.horizontal(|ui| {
ui.label("Overlap: ").on_hover_text("If we recently loaded events up to time T, but restarted, we will now load events starting from time T minus overlap. Takes effect on restart. I recommend 300 (5 minutes).");
ui.add(Slider::new(&mut app.settings.overlap, 0..=3600).text("seconds, "));
ui.label(secs_to_string(app.settings.overlap));
});
ui.checkbox(
&mut app.settings.recompute_feed_periodically,
"Recompute feed periodically. If this is off, you will get a refresh button",
);
ui.horizontal(|ui| {
ui.label("Recompute feed every (milliseconds): ")
.on_hover_text(
"The UI redraws frequently. We recompute the feed less frequently to conserve CPU. Takes effect when the feed next recomputes. I recommend 3500.",
);
ui.add(Slider::new(&mut app.settings.feed_recompute_interval_ms, 1000..=12000).text("milliseconds"));
});
ui.add_space(10.0);
ui.heading("Event Selection Settings");
ui.add_space(10.0);
ui.checkbox(
&mut app.settings.reactions,
"Enable reactions (show and react)",
);
ui.checkbox(&mut app.settings.enable_zap_receipts, "Enable zap receipts");
ui.checkbox(&mut app.settings.reposts, "Enable reposts (show)");
ui.checkbox(&mut app.settings.direct_messages, "Show Direct Messages")
.on_hover_text("Takes effect fully only on restart.");
ui.checkbox(&mut app.settings.show_long_form, "Show Long-Form Posts")
.on_hover_text("Takes effect fully only on restart.");
ui.add_space(10.0);
ui.heading("Event Content Settings");
ui.add_space(10.0);
ui.checkbox(&mut app.settings.show_mentions, "Render mentions inline")
.on_hover_text(if app.settings.show_mentions {
"Disable to just show a link to a mentioned post where it appears in the text"
} else {
"Enable to render a mentioned post where it appears in the text"
});
ui.checkbox(
&mut app.settings.show_media,
"Render all media inline automatically",
)
.on_hover_text(
"If off, you have to click to (potentially fetch and) render media inline. If on, all media referenced by posts in your feed will be (potentially fetched and) rendered. However, if Fetch Media is disabled, only cached media can be shown as media will not be fetched."
);
}
fn secs_to_string(secs: u64) -> String {
let days = secs / 86400;
let remainder = secs % 86400;
let hours = remainder / 3600;
let remainder = remainder % 3600;
let minutes = remainder / 60;
let seconds = remainder % 60;
let mut output: String = String::new();
if days > 0 {
output.push_str(&format!(" {} days", days));
}
if hours > 0 {
output.push_str(&format!(" {} hours", hours));
}
if minutes > 0 {
output.push_str(&format!(" {} minutes", minutes));
}
output.push_str(&format!(" {} seconds", seconds));
output
}

View File

@ -0,0 +1,29 @@
use crate::comms::ToOverlordMessage;
use crate::ui::GossipUi;
use crate::GLOBALS;
use eframe::egui;
use egui::widgets::Slider;
use egui::{Context, Ui};
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.heading("Database Settings");
ui.add_space(20.0);
ui.label("Prune period");
ui.add(Slider::new(&mut app.settings.prune_period_days, 7..=360).text("days"));
// Only let them prune after they have saved
if let Ok(Some(stored_settings)) = GLOBALS.storage.read_settings() {
if stored_settings == app.settings {
ui.add_space(20.0);
if ui.button("Prune Database Now")
.on_hover_text("This will delete events older than the prune period. but the LMDB files will continue consuming disk space. To compact them, copy withem with `mdb_copy -c` when gossip is not running.")
.clicked() {
GLOBALS.status_queue.write().write(
"Pruning database, please wait...".to_owned()
);
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PruneDatabase);
}
}
}
}

32
src/ui/settings/id.rs Normal file
View File

@ -0,0 +1,32 @@
use crate::ui::{GossipUi, Page};
use eframe::egui;
use egui::widgets::Slider;
use egui::{Context, Ui};
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.heading("Identity Settings");
ui.add_space(20.0);
// public_key
ui.horizontal(|ui| {
ui.label("Public Key:");
if let Some(pk) = app.settings.public_key {
ui.label(pk.as_bech32_string());
} else {
ui.label("NOT SET");
}
});
ui.horizontal(|ui| {
ui.label("Manage your public key identity on the");
if ui.link("Account > Keys").clicked() {
app.set_page(Page::YourKeys);
}
ui.label("page.");
});
// log_n
ui.add_space(20.0);
ui.label("Encrypted Private Key scrypt N parameter");
ui.label("(NOTE: changing this will not re-encrypt any existing encrypted private key)");
ui.add(Slider::new(&mut app.settings.log_n, 18..=22).text("logN iteratons"));
}

99
src/ui/settings/mod.rs Normal file
View File

@ -0,0 +1,99 @@
use crate::comms::ToOverlordMessage;
use crate::ui::{GossipUi, SettingsTab};
use crate::GLOBALS;
use eframe::egui;
use egui::{Align, Context, Layout, ScrollArea, Ui, Vec2};
mod content;
mod database;
mod id;
mod network;
mod posting;
mod ui;
pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Frame, ui: &mut Ui) {
ui.heading("Settings");
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
if let Ok(Some(stored_settings)) = GLOBALS.storage.read_settings() {
if stored_settings != app.settings {
if ui.button("REVERT CHANGES").clicked() {
app.settings = GLOBALS.settings.read().clone();
// Fully revert any DPI changes
match app.settings.override_dpi {
Some(value) => {
app.override_dpi = true;
app.override_dpi_value = value;
}
None => {
app.override_dpi = false;
app.override_dpi_value = app.original_dpi_value;
}
};
let ppt: f32 = app.override_dpi_value as f32 / 72.0;
ctx.set_pixels_per_point(ppt);
}
if ui.button("SAVE CHANGES").clicked() {
// Apply DPI change
if stored_settings.override_dpi != app.settings.override_dpi {
if let Some(value) = app.settings.override_dpi {
let ppt: f32 = value as f32 / 72.0;
ctx.set_pixels_per_point(ppt);
}
}
// Save new original DPI value
if let Some(value) = app.settings.override_dpi {
app.original_dpi_value = value;
}
// Copy local settings to global settings
*GLOBALS.settings.write() = app.settings.clone();
// Tell the overlord to save them
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::SaveSettings);
}
}
}
});
ui.add_space(10.0);
ui.separator();
ScrollArea::vertical()
.id_source("settings")
.override_scroll_delta(Vec2 {
x: 0.0,
y: app.current_scroll_offset,
})
.show(ui, |ui| {
ui.horizontal_wrapped(|ui| {
ui.selectable_value(&mut app.settings_tab, SettingsTab::Id, "Identity");
ui.label("|");
ui.selectable_value(&mut app.settings_tab, SettingsTab::Ui, "Ui");
ui.label("|");
ui.selectable_value(&mut app.settings_tab, SettingsTab::Content, "Content");
ui.label("|");
ui.selectable_value(&mut app.settings_tab, SettingsTab::Network, "Network");
ui.label("|");
ui.selectable_value(&mut app.settings_tab, SettingsTab::Posting, "Posting");
ui.label("|");
ui.selectable_value(&mut app.settings_tab, SettingsTab::Database, "Database");
});
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
match app.settings_tab {
SettingsTab::Content => content::update(app, ctx, frame, ui),
SettingsTab::Database => database::update(app, ctx, frame, ui),
SettingsTab::Id => id::update(app, ctx, frame, ui),
SettingsTab::Network => network::update(app, ctx, frame, ui),
SettingsTab::Posting => posting::update(app, ctx, frame, ui),
SettingsTab::Ui => ui::update(app, ctx, frame, ui),
}
});
}

183
src/ui/settings/network.rs Normal file
View File

@ -0,0 +1,183 @@
use crate::ui::{GossipUi, Page};
use eframe::egui;
use egui::widgets::Slider;
use egui::{Context, Ui};
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.heading("Network Settings");
ui.add_space(10.0);
ui.checkbox(&mut app.settings.offline, "Offline Mode")
.on_hover_text("If selected, no network requests will be issued. Takes effect on restart.");
ui.checkbox(&mut app.settings.load_avatars, "Fetch Avatars")
.on_hover_text("If disabled, avatars will not be fetched, but cached avatars will still display. Takes effect on save.");
ui.checkbox(&mut app.settings.load_media, "Fetch Media")
.on_hover_text("If disabled, no new media will be fetched, but cached media will still display. Takes effect on save.");
ui.checkbox(&mut app.settings.check_nip05, "Check NIP-05")
.on_hover_text("If disabled, NIP-05 fetches will not be performed, but existing knowledge will be preserved, and following someone by NIP-05 will override this and do the fetch. Takes effect on save.");
ui.checkbox(&mut app.settings.automatically_fetch_metadata, "Automatically Fetch Metadata")
.on_hover_text("If enabled, metadata that is entirely missing will be fetched as you scroll past people. Existing metadata won't be updated. Takes effect on save.");
ui.add_space(10.0);
ui.heading("Relay Settings");
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.label("Manage individual relays on the");
if ui.link("Relays > Configure").clicked() {
app.set_page(Page::RelaysKnownNetwork);
}
ui.label("page.");
});
ui.horizontal(|ui| {
ui.label("Number of relays to query per person: ").on_hover_text("We will query N relays per person. Many people share the same relays so those will be queried about multiple people. Takes affect on restart. I recommend 2. Too many and gossip will (currently) keep connecting to new relays trying to find the unfindable, loading many events from each. Takes effect on restart.");
ui.add(Slider::new(&mut app.settings.num_relays_per_person, 1..=4).text("relays"));
});
ui.horizontal(|ui| {
ui.label("Maximum following feed relays: ")
.on_hover_text(
"We will not stay connected to more than this many relays for following feed. Takes affect on restart. During these early days of nostr, I recommend capping this at around 20 to 30.",
);
ui.add(Slider::new(&mut app.settings.max_relays, 5..=100).text("relays"));
});
ui.add_space(10.0);
ui.heading("HTTP Fetch Settings");
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.label("Looptime for metadata fetcher thread");
ui.add(Slider::new(&mut app.settings.fetcher_metadata_looptime_ms, 1000..=6000).text("ms"));
});
ui.horizontal(|ui| {
ui.label("Looptime for general fetcher thread");
ui.add(Slider::new(&mut app.settings.fetcher_looptime_ms, 1000..=6000).text("ms"));
});
ui.horizontal(|ui| {
ui.label("HTTP Connect Timeout");
ui.add(Slider::new(&mut app.settings.fetcher_connect_timeout_sec, 5..=120).text("seconds"));
});
ui.horizontal(|ui| {
ui.label("HTTP Idle Timeout");
ui.add(Slider::new(&mut app.settings.fetcher_timeout_sec, 5..=120).text("seconds"));
});
ui.horizontal(|ui| {
ui.label("Max simultaneous HTTP requests per remote host")
.on_hover_text(
"If you set this too high, you may start getting 403-Forbidden or \
429-TooManyRequests errors from the remote host",
);
ui.add(
Slider::new(&mut app.settings.fetcher_max_requests_per_host, 1..=10).text("requests"),
);
});
ui.horizontal(|ui| {
ui.label("How long to avoid contacting a host after a minor error");
ui.add(
Slider::new(
&mut app.settings.fetcher_host_exclusion_on_low_error_secs,
10..=60,
)
.text("seconds"),
);
});
ui.horizontal(|ui| {
ui.label("How long to avoid contacting a host after a medium error");
ui.add(
Slider::new(
&mut app.settings.fetcher_host_exclusion_on_med_error_secs,
20..=180,
)
.text("seconds"),
);
});
ui.horizontal(|ui| {
ui.label("How long to avoid contacting a host after a major error");
ui.add(
Slider::new(
&mut app.settings.fetcher_host_exclusion_on_high_error_secs,
60..=1800,
)
.text("seconds"),
);
});
ui.horizontal(|ui| {
ui.label("When a NIP-11 is not a NIP-11, how many lines of the body do you want to see?");
ui.add(
Slider::new(&mut app.settings.nip11_lines_to_output_on_error, 1..=100).text("lines"),
);
});
ui.add_space(10.0);
ui.heading("Websocket Settings");
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.label("Maximum websocket message size");
ui.add(
Slider::new(&mut app.settings.max_websocket_message_size_kb, 256..=4096).text("KiB"),
);
});
ui.horizontal(|ui| {
ui.label("Maximum websocket frame size");
ui.add(Slider::new(&mut app.settings.max_websocket_frame_size_kb, 256..=4096).text("KiB"));
});
ui.checkbox(&mut app.settings.websocket_accept_unmasked_frames, "Accept unmasked websocket frames?")
.on_hover_text("This is contrary to the standard, but some incorrect software/libraries may use unmasked frames.");
ui.horizontal(|ui| {
ui.label("Websocket Connect Timeout");
ui.add(
Slider::new(&mut app.settings.websocket_connect_timeout_sec, 5..=120).text("seconds"),
);
});
ui.horizontal(|ui| {
ui.label("Websocket Ping Frequency");
ui.add(
Slider::new(&mut app.settings.websocket_ping_frequency_sec, 30..=600).text("seconds"),
);
});
ui.add_space(10.0);
ui.heading("Stale Time Settings");
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.label("How long before a relay list becomes stale and needs rechecking?");
ui.add(Slider::new(&mut app.settings.relay_list_becomes_stale_hours, 2..=40).text("hours"));
});
ui.horizontal(|ui| {
ui.label("How long before metadata becomes stale and needs rechecking?");
ui.add(Slider::new(&mut app.settings.metadata_becomes_stale_hours, 2..=40).text("hours"));
});
ui.horizontal(|ui| {
ui.label("How long before valid nip05 becomes stale and needs rechecking?");
ui.add(
Slider::new(&mut app.settings.nip05_becomes_stale_if_valid_hours, 2..=40).text("hours"),
);
});
ui.horizontal(|ui| {
ui.label("How long before invalid nip05 becomes stale and needs rechecking?");
ui.add(
Slider::new(
&mut app.settings.nip05_becomes_stale_if_invalid_minutes,
5..=600,
)
.text("minutes"),
);
});
ui.horizontal(|ui| {
ui.label("How long before an avatar image becomes stale and needs rechecking?");
ui.add(Slider::new(&mut app.settings.avatar_becomes_stale_hours, 2..=40).text("hours"));
});
ui.horizontal(|ui| {
ui.label("How long before event media becomes stale and needs rechecking?");
ui.add(Slider::new(&mut app.settings.media_becomes_stale_hours, 2..=40).text("hours"));
});
}

View File

@ -0,0 +1,30 @@
use crate::ui::GossipUi;
use eframe::egui;
use egui::widgets::Slider;
use egui::{Context, Ui};
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.heading("Posting Settings");
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.label("Proof of Work: ")
.on_hover_text("The larger the number, the longer it takes.");
ui.add(Slider::new(&mut app.settings.pow, 0..=40).text("leading zero bits"));
});
ui.checkbox(
&mut app.settings.set_client_tag,
"Add tag [\"client\",\"gossip\"] to posts",
)
.on_hover_text("Takes effect immediately.");
ui.checkbox(
&mut app.settings.set_user_agent,
&format!(
"Send User-Agent Header to Relays: gossip/{}",
app.about.version
),
)
.on_hover_text("Takes effect on next relay connection.");
}

91
src/ui/settings/ui.rs Normal file
View File

@ -0,0 +1,91 @@
use crate::ui::{GossipUi, ThemeVariant};
use eframe::egui;
use egui::widgets::{Button, Slider};
use egui::{Context, Ui};
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.heading("UI Settings");
ui.add_space(20.0);
ui.checkbox(
&mut app.settings.highlight_unread_events,
"Highlight unread events",
);
ui.checkbox(
&mut app.settings.posting_area_at_top,
"Show posting area at the top instead of the bottom",
);
ui.add_space(20.0);
ui.horizontal(|ui| {
ui.label("Theme:");
if !app.settings.theme.follow_os_dark_mode {
if app.settings.theme.dark_mode {
if ui
.add(Button::new("🌙 Dark"))
.on_hover_text("Switch to light mode")
.clicked()
{
app.settings.theme.dark_mode = false;
crate::ui::theme::apply_theme(app.settings.theme, ctx);
}
} else {
if ui
.add(Button::new("☀ Light"))
.on_hover_text("Switch to dark mode")
.clicked()
{
app.settings.theme.dark_mode = true;
crate::ui::theme::apply_theme(app.settings.theme, ctx);
}
}
}
let theme_combo = egui::ComboBox::from_id_source("Theme");
theme_combo
.selected_text( app.settings.theme.name() )
.show_ui(ui, |ui| {
for theme_variant in ThemeVariant::all() {
if ui.add(egui::widgets::SelectableLabel::new(*theme_variant == app.settings.theme.variant, theme_variant.name())).clicked() {
app.settings.theme.variant = *theme_variant;
crate::ui::theme::apply_theme(app.settings.theme, ctx);
};
}
});
ui.checkbox(&mut app.settings.theme.follow_os_dark_mode, "Follow OS dark-mode")
.on_hover_text("Follow the operating system setting for dark-mode (requires app-restart to take effect)");
});
ui.add_space(20.0);
ui.horizontal(|ui| {
ui.label("Override DPI: ")
.on_hover_text(
"On some systems, DPI is not reported properly. In other cases, people like to zoom in or out. This lets you.",
);
ui.checkbox(
&mut app.override_dpi,
"Override to ");
ui.add(Slider::new(&mut app.override_dpi_value, 72..=250).text("DPI"));
if ui.button("Apply this change now (without saving)").clicked() {
let ppt: f32 = app.override_dpi_value as f32 / 72.0;
ctx.set_pixels_per_point(ppt);
}
// transfer to app.settings
app.settings.override_dpi = if app.override_dpi {
// Set it in settings to be saved on button press
Some(app.override_dpi_value)
} else {
None
};
});
ui.add_space(20.0);
ui.horizontal(|ui| {
ui.label("Maximum FPS: ")
.on_hover_text(
"The UI redraws every frame. By limiting the maximum FPS you can reduce load on your CPU. Takes effect immediately. I recommend 10, maybe even less.",
);
ui.add(Slider::new(&mut app.settings.max_fps, 2..=60).text("Frames per second"));
});
}

View File

@ -120,7 +120,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
}
ui.label("to edit/save metadata.");
});
} else if !GLOBALS
} else if GLOBALS
.storage
.filter_relays(|r| r.has_usage_bits(Relay::WRITE))
.unwrap_or(vec![])