[MIGRATION] Merge branch 'settings' into unstable

This commit is contained in:
Mike Dilger 2023-08-13 12:56:43 +12:00
commit 175a33cdb4
24 changed files with 1316 additions and 539 deletions

View File

@ -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 {
@ -346,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
}
@ -183,11 +164,6 @@ impl Fetcher {
// 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);
@ -271,14 +247,6 @@ impl Fetcher {
}
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;
}
// Do not fetch if offline
if GLOBALS.settings.read().offline {
tracing::debug!("FETCH {url}: Failed: offline mode");
@ -312,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());
@ -384,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,
@ -391,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() {
@ -410,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 {
@ -421,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);
@ -501,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

@ -121,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);
@ -158,8 +157,18 @@ pub(crate) fn load_image_bytes(
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,
};
// This preserves aspect ratio. The sizes represent bounds.
image = image.resize(default_size, default_size, FilterType::Triangle);
image = image.resize(default_size, default_size, algo);
}
let current_size = [image.width() as _, image.height() as _];
let image_buffer = image.into_rgba8();

View File

@ -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")
);
}
}
@ -182,13 +190,16 @@ 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 connect_timeout = GLOBALS.settings.read().websocket_connect_timeout_sec;
let (websocket_stream, response) = tokio::time::timeout(
std::time::Duration::new(15, 0),
std::time::Duration::new(connect_timeout, 0),
tokio_tungstenite::connect_async_with_config(req, Some(config), false),
)
.await??;
@ -250,7 +261,10 @@ 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.

View File

@ -108,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()?;
@ -670,7 +670,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!(

View File

@ -198,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;
@ -224,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 {
@ -270,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;
@ -394,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 {
@ -464,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)) => {

View File

@ -46,7 +46,7 @@ pub async fn process_new_event(
if let Some(ref relay_url) = seen_on {
// Verify the event
let mut maxtime = now;
maxtime.0 += 60 * 15; // 15 minutes into the future
maxtime.0 += GLOBALS.settings.read().future_allowance_secs as i64;
if let Err(e) = event.verify(Some(maxtime)) {
tracing::error!(
"{}: VERIFY ERROR: {}, {}",

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,
}
}
}

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>>,
@ -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

@ -4,11 +4,13 @@ use lmdb::{Cursor, RwTransaction, Transaction};
use nostr_types::Event;
use speedy::Readable;
mod settings;
impl Storage {
const MIGRATION_LEVEL: u32 = 1;
const MAX_MIGRATION_LEVEL: u32 = 2;
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
@ -17,47 +19,48 @@ impl Storage {
}
let mut txn = self.env.begin_rw_txn()?;
while level < Self::MIGRATION_LEVEL {
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, Some(&mut txn))?;
self.write_migration_level(level, Some(&mut txn))?;
}
txn.commit()?;
Ok(())
}
fn migrate_inner<'a>(
&'a self,
level: u32,
rw_txn: Option<&mut RwTransaction<'a>>,
) -> Result<(), Error> {
fn migrate_inner<'a>(&'a self, level: u32, txn: &mut RwTransaction<'a>) -> Result<(), Error> {
let prefix = format!("LMDB Migration {} -> {}", level, level + 1);
match level {
0 => Ok(()),
1 => self.compute_relationships(rw_txn),
n => panic!("Unknown migration level {}", n),
}
0 => {
let total = self.get_event_stats()?.entries();
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))?;
}
_ => panic!("Unreachable migration level"),
};
tracing::info!("done.");
Ok(())
}
// Load and process every event in order to generate the relationships data
fn compute_relationships<'a>(
&'a self,
total: usize,
rw_txn: Option<&mut RwTransaction<'a>>,
) -> Result<(), Error> {
self.disable_sync()?;
let f = |txn: &mut RwTransaction<'a>| -> Result<(), Error> {
// track progress
let total = self.get_event_stats()?.entries();
tracing::info!(
"LMDB Migration 1: Computing and storing event relationships for {} events...",
total
);
let mut count = 0;
let event_txn = self.env.begin_ro_txn()?;

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 lmdb::{RwTransaction, Transaction, WriteFlags};
use speedy::{Readable, Writable};
impl Storage {
pub(in crate::storage) fn try_migrate_settings1_settings2<'a>(
&'a self,
rw_txn: Option<&mut RwTransaction<'a>>,
) -> Result<(), Error> {
let f = |txn: &mut RwTransaction<'a>| -> Result<(), Error> {
// If something is under the old "settings" key
if let Ok(bytes) = txn.get(self.general, 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
txn.put(self.general, b"settings2", &bytes, WriteFlags::empty())?;
// Then delete the old "settings" key
txn.del(self.general, b"settings", None)?;
}
Ok(())
};
match rw_txn {
Some(txn) => f(txn)?,
None => {
let mut txn = self.env.begin_rw_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,
}

View File

@ -113,6 +113,8 @@ impl Storage {
// Some filesystem that doesn't handle sparse files may allocate all
// of this, so we don't go too crazy big.
// NOTE: this cannot be a setting because settings are only available
// after the database has been launched.
builder.set_map_size(1048576 * 1024 * 24); // 24 GB
let env = builder.open(&Profile::current()?.lmdb_dir)?;
@ -498,7 +500,7 @@ impl Storage {
let bytes = settings.write_to_vec()?;
let f = |txn: &mut RwTransaction<'a>| -> Result<(), Error> {
txn.put(self.general, b"settings", &bytes, WriteFlags::empty())?;
txn.put(self.general, b"settings2", &bytes, WriteFlags::empty())?;
Ok(())
};
@ -516,7 +518,7 @@ impl Storage {
pub fn read_settings(&self) -> Result<Option<Settings>, Error> {
let txn = self.env.begin_ro_txn()?;
match txn.get(self.general, b"settings") {
match txn.get(self.general, b"settings2") {
Ok(bytes) => Ok(Some(Settings::read_from_buffer(bytes)?)),
Err(lmdb::Error::NotFound) => Ok(None),
Err(e) => Err(e.into()),

View File

@ -134,6 +134,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();
@ -168,6 +178,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,
@ -195,6 +206,7 @@ struct GossipUi {
inbox_include_indirect: bool,
submenu_ids: HashMap<SubMenu, egui::Id>,
submenu_state: SubMenuState,
settings_tab: SettingsTab,
// General Data
about: About,
@ -392,6 +404,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(),
@ -414,6 +427,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,
@ -576,6 +590,55 @@ impl eframe::App for GossipUi {
}
}
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_stats()
.map(|s| s.entries())
.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(
@ -825,41 +888,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 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");
});
});
*/
});
// Prepare local zap data once per frame for easier compute at render time

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::RelaysAll);
}
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"));
});
}