Settings Manager (#211)

* refactor: landing screen

* fix: code debt

* feat: add settings screen

* chore: clean up

* feat: settings

* feat: small updates
This commit is contained in:
雨宮蓮
2024-06-19 14:00:58 +07:00
committed by GitHub
parent 0061ecea78
commit 18c133d096
50 changed files with 937 additions and 1167 deletions

View File

@@ -1,14 +1,18 @@
use std::path::PathBuf;
use std::str::FromStr;
#[cfg(target_os = "macos")]
use cocoa::{appkit::NSApp, base::nil, foundation::NSString};
use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl};
use tauri::{LogicalPosition, LogicalSize, Manager, State, WebviewUrl};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::utils::config::WindowEffectsConfig;
use tauri::WebviewWindowBuilder;
use tauri::window::Effect;
use tauri_plugin_decorum::WebviewWindowExt;
use url::Url;
use crate::Nostr;
#[tauri::command]
#[specta::specta]
@@ -20,18 +24,32 @@ pub fn create_column(
height: f32,
url: &str,
app_handle: tauri::AppHandle,
state: State<'_, Nostr>,
) -> Result<String, String> {
let settings = state.settings.lock().unwrap().clone();
match app_handle.get_window("main") {
Some(main_window) => match app_handle.get_webview(label) {
Some(_) => Ok(label.into()),
None => {
let path = PathBuf::from(url);
let webview_url = WebviewUrl::App(path);
let builder = tauri::webview::WebviewBuilder::new(label, webview_url)
.user_agent("Lume/4.0")
.zoom_hotkeys_enabled(true)
.enable_clipboard_access()
.transparent(true);
let builder = match settings.proxy {
Some(url) => {
let proxy = Url::from_str(&url).unwrap();
tauri::webview::WebviewBuilder::new(label, webview_url)
.user_agent("Lume/4.0")
.zoom_hotkeys_enabled(true)
.enable_clipboard_access()
.transparent(true)
.proxy_url(proxy)
}
None => tauri::webview::WebviewBuilder::new(label, webview_url)
.user_agent("Lume/4.0")
.zoom_hotkeys_enabled(true)
.enable_clipboard_access()
.transparent(true),
};
match main_window.add_child(
builder,
LogicalPosition::new(x, y),

View File

@@ -15,9 +15,11 @@ use std::{
str::FromStr,
};
use std::sync::Mutex;
use std::time::Duration;
use nostr_sdk::prelude::*;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{Manager, path::BaseDirectory};
#[cfg(target_os = "macos")]
use tauri::tray::{MouseButtonState, TrayIconEvent};
@@ -39,10 +41,39 @@ pub struct Nostr {
#[serde(skip_serializing)]
client: Client,
contact_list: Mutex<Vec<Contact>>,
settings: Mutex<Settings>,
}
#[derive(Clone, Serialize, Deserialize, Type)]
pub struct Settings {
proxy: Option<String>,
image_resize_service: Option<String>,
use_relay_hint: bool,
content_warning: bool,
display_avatar: bool,
display_zap_button: bool,
display_repost_button: bool,
display_media: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
proxy: None,
image_resize_service: Some("https://wsrv.nl/".into()),
use_relay_hint: true,
content_warning: true,
display_avatar: true,
display_zap_button: true,
display_repost_button: true,
display_media: true,
}
}
}
fn main() {
let mut ctx = tauri::generate_context!();
let invoke_handler = {
let builder = tauri_specta::ts::builder().commands(tauri_specta::collect_commands![
nostr::relay::get_relays,
@@ -57,8 +88,7 @@ fn main() {
nostr::keys::get_private_key,
nostr::keys::connect_remote_account,
nostr::keys::load_account,
nostr::keys::verify_nip05,
nostr::metadata::get_current_user_profile,
nostr::metadata::get_current_profile,
nostr::metadata::get_profile,
nostr::metadata::get_contact_list,
nostr::metadata::set_contact_list,
@@ -75,6 +105,9 @@ fn main() {
nostr::metadata::zap_event,
nostr::metadata::friend_to_friend,
nostr::metadata::get_notifications,
nostr::metadata::get_settings,
nostr::metadata::set_new_settings,
nostr::metadata::verify_nip05,
nostr::event::get_event_meta,
nostr::event::get_event,
nostr::event::get_event_from,
@@ -95,8 +128,8 @@ fn main() {
commands::window::reposition_column,
commands::window::resize_column,
commands::window::open_window,
commands::window::set_badge,
commands::window::open_main_window
commands::window::open_main_window,
commands::window::set_badge
]);
#[cfg(debug_assertions)]
@@ -153,9 +186,15 @@ fn main() {
let _ = fs::create_dir_all(home_dir.join("Lume/"));
tauri::async_runtime::block_on(async move {
// Create nostr connection
// Setup database
let database = SQLiteDatabase::open(home_dir.join("Lume/lume.db")).await;
let opts = Options::new().automatic_authentication(true);
// Config
let opts = Options::new()
.automatic_authentication(true)
.connection_timeout(Some(Duration::from_secs(5)));
// Setup nostr client
let client = match database {
Ok(db) => ClientBuilder::default().database(db).opts(opts).build(),
Err(_) => ClientBuilder::default().opts(opts).build(),
@@ -197,6 +236,7 @@ fn main() {
app.handle().manage(Nostr {
client,
contact_list: Mutex::new(vec![]),
settings: Mutex::new(Settings::default()),
})
});
@@ -218,7 +258,7 @@ fn main() {
.invoke_handler(invoke_handler)
.build(ctx)
.expect("error while running tauri application")
.run(|app, event| {
.run(|_, event| {
if let tauri::RunEvent::ExitRequested { api, .. } = event {
// Hide app icon on macOS
// let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory);

View File

@@ -26,22 +26,93 @@ pub async fn get_event_meta(content: &str) -> Result<Meta, ()> {
#[specta::specta]
pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<RichEvent, String> {
let client = &state.client;
let event_id: Option<EventId> = match Nip19::from_bech32(id) {
let event_id = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::EventId(id) => Some(id),
Nip19::Event(event) => Some(event.event_id),
_ => None,
Nip19::EventId(id) => id,
Nip19::Event(event) => event.event_id,
_ => return Err("Event ID is not valid.".into()),
},
Err(_) => match EventId::from_hex(id) {
Ok(val) => Some(val),
Err(_) => None,
Ok(id) => id,
Err(_) => return Err("Event ID is not valid.".into()),
},
};
match event_id {
Some(id) => {
match client
.get_events_of(vec![Filter::new().id(event_id)], None)
.await
{
Ok(events) => {
if let Some(event) = events.first() {
let raw = event.as_json();
let parsed = if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await)
} else {
None
};
Ok(RichEvent { raw, parsed })
} else {
Err("Cannot found this event with current relay list".into())
}
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_event_from(
id: &str,
relay_hint: &str,
state: State<'_, Nostr>,
) -> Result<RichEvent, String> {
let client = &state.client;
let settings = state.settings.lock().unwrap().clone();
let event_id = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::EventId(id) => id,
Nip19::Event(event) => event.event_id,
_ => return Err("Event ID is not valid.".into()),
},
Err(_) => match EventId::from_hex(id) {
Ok(id) => id,
Err(_) => return Err("Event ID is not valid.".into()),
},
};
if !settings.use_relay_hint {
match client
.get_events_of(vec![Filter::new().id(event_id)], None)
.await
{
Ok(events) => {
if let Some(event) = events.first() {
let raw = event.as_json();
let parsed = if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await)
} else {
None
};
Ok(RichEvent { raw, parsed })
} else {
return Err("Cannot found this event with current relay list".into());
}
}
Err(err) => return Err(err.to_string()),
}
} else {
// Add relay hint to relay pool
if let Err(err) = client.add_relay(relay_hint).await {
return Err(err.to_string());
}
if client.connect_relay(relay_hint).await.is_ok() {
match client
.get_events_of(vec![Filter::new().id(id)], Some(Duration::from_secs(10)))
.get_events_from(vec![relay_hint], vec![Filter::new().id(event_id)], None)
.await
{
Ok(events) => {
@@ -60,65 +131,9 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<RichEvent, S
}
Err(err) => Err(err.to_string()),
}
} else {
Err("Relay connection failed.".into())
}
None => Err("Event ID is not valid.".into()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_event_from(
id: &str,
relay_hint: &str,
state: State<'_, Nostr>,
) -> Result<RichEvent, String> {
let client = &state.client;
let event_id: Option<EventId> = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::EventId(id) => Some(id),
Nip19::Event(event) => Some(event.event_id),
_ => None,
},
Err(_) => match EventId::from_hex(id) {
Ok(val) => Some(val),
Err(_) => None,
},
};
// Add relay hint to relay pool
if let Err(err) = client.add_relay(relay_hint).await {
return Err(err.to_string());
}
if client.connect_relay(relay_hint).await.is_ok() {
match event_id {
Some(id) => {
match client
.get_events_from(vec![relay_hint], vec![Filter::new().id(id)], None)
.await
{
Ok(events) => {
if let Some(event) = events.first() {
let raw = event.as_json();
let parsed = if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await)
} else {
None
};
Ok(RichEvent { raw, parsed })
} else {
Err("Cannot found this event with current relay list".into())
}
}
Err(err) => Err(err.to_string()),
}
}
None => Err("Event ID is not valid.".into()),
}
} else {
Err("Relay connection failed.".into())
}
}
@@ -225,7 +240,7 @@ pub async fn get_local_events(
.await
{
Ok(events) => {
let dedup = dedup_event(&events, false);
let dedup = dedup_event(&events);
let futures = dedup.into_iter().map(|ev| async move {
let raw = ev.as_json();
@@ -282,7 +297,7 @@ pub async fn get_group_events(
.await
{
Ok(events) => {
let dedup = dedup_event(&events, false);
let dedup = dedup_event(&events);
let futures = dedup.into_iter().map(|ev| async move {
let raw = ev.as_json();
@@ -325,7 +340,7 @@ pub async fn get_global_events(
.await
{
Ok(events) => {
let dedup = dedup_event(&events, false);
let dedup = dedup_event(&events);
let futures = dedup.into_iter().map(|ev| async move {
let raw = ev.as_json();
let parsed = if ev.kind == Kind::TextNote {
@@ -364,7 +379,7 @@ pub async fn get_hashtag_events(
match client.get_events_of(vec![filter], None).await {
Ok(events) => {
let dedup = dedup_event(&events, false);
let dedup = dedup_event(&events);
let futures = dedup.into_iter().map(|ev| async move {
let raw = ev.as_json();
let parsed = if ev.kind == Kind::TextNote {

View File

@@ -1,4 +1,3 @@
use std::str::FromStr;
use std::time::Duration;
use keyring::Entry;
@@ -9,7 +8,7 @@ use specta::Type;
use tauri::{EventTarget, Manager, State};
use tauri_plugin_notification::NotificationExt;
use crate::Nostr;
use crate::{Nostr, Settings};
#[derive(Serialize, Type)]
pub struct Account {
@@ -109,7 +108,6 @@ pub async fn load_account(
state: State<'_, Nostr>,
app: tauri::AppHandle,
) -> Result<bool, String> {
let handle = app.clone();
let client = &state.client;
let keyring = Entry::new(npub, "nostr_secret").unwrap();
@@ -141,15 +139,6 @@ pub async fn load_account(
let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
// Get user's contact list
let contacts = client
.get_contact_list(Some(Duration::from_secs(10)))
.await
.unwrap();
// Update state
*state.contact_list.lock().unwrap() = contacts;
// Connect to user's relay (NIP-65)
if let Ok(events) = client
.get_events_of(
@@ -189,7 +178,49 @@ pub async fn load_account(
}
};
// Get user's contact list
let contacts = client
.get_contact_list(Some(Duration::from_secs(10)))
.await
.unwrap();
// Update state
*state.contact_list.lock().unwrap() = contacts;
// Get user's settings
let handle = app.clone();
// Spawn a thread to handle it
tauri::async_runtime::spawn(async move {
let window = handle.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
let ident = "lume:settings";
let filter = Filter::new()
.author(public_key)
.kind(Kind::ApplicationSpecificData)
.identifier(ident)
.limit(1);
if let Ok(events) = client
.get_events_of(vec![filter], Some(Duration::from_secs(5)))
.await
{
if let Some(event) = events.first() {
let content = event.content();
if let Ok(decrypted) = signer.nip44_decrypt(public_key, content).await {
let parsed: Settings =
serde_json::from_str(&decrypted).expect("Could not parse settings payload");
*state.settings.lock().unwrap() = parsed;
}
}
}
});
// Run sync service
let handle = app.clone();
// Spawn a thread to handle it
tauri::async_runtime::spawn(async move {
let window = handle.get_window("main").unwrap();
let state = window.state::<Nostr>();
@@ -212,6 +243,7 @@ pub async fn load_account(
});
// Run notification service
// Spawn a thread to handle it
tauri::async_runtime::spawn(async move {
println!("Starting notification service...");
@@ -316,7 +348,7 @@ pub async fn load_account(
Ok(true)
} else {
Err("Key not found.".into())
Err("Cancelled".into())
}
}
@@ -382,15 +414,3 @@ pub fn get_private_key(npub: &str) -> Result<String, String> {
Err("Key not found".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, String> {
match PublicKey::from_str(key) {
Ok(public_key) => {
let status = nip05::verify(&public_key, nip05, None).await;
Ok(status.is_ok())
}
Err(err) => Err(err.to_string()),
}
}

View File

@@ -4,13 +4,13 @@ use keyring::Entry;
use nostr_sdk::prelude::*;
use tauri::State;
use crate::Nostr;
use crate::{Nostr, Settings};
use super::get_latest_event;
#[tauri::command]
#[specta::specta]
pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<String, String> {
pub async fn get_current_profile(state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
let signer = match client.signer().await {
@@ -566,3 +566,31 @@ pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, S
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_settings(state: State<'_, Nostr>) -> Result<Settings, ()> {
let settings = state.settings.lock().unwrap().clone();
Ok(settings)
}
#[tauri::command]
#[specta::specta]
pub async fn set_new_settings(settings: &str, state: State<'_, Nostr>) -> Result<(), ()> {
let parsed: Settings = serde_json::from_str(settings).expect("Could not parse settings payload");
*state.settings.lock().unwrap() = parsed;
Ok(())
}
#[tauri::command]
#[specta::specta]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, String> {
match PublicKey::from_str(key) {
Ok(public_key) => {
let status = nip05::verify(&public_key, nip05, None).await;
Ok(status.is_ok())
}
Err(err) => Err(err.to_string()),
}
}

View File

@@ -50,7 +50,7 @@ pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
events.iter().max_by_key(|event| event.created_at())
}
pub fn dedup_event(events: &[Event], nsfw: bool) -> Vec<Event> {
pub fn dedup_event(events: &[Event]) -> Vec<Event> {
let mut seen_ids = HashSet::new();
events
.iter()
@@ -64,16 +64,7 @@ pub fn dedup_event(events: &[Event], nsfw: bool) -> Vec<Event> {
seen_ids.insert(*id);
}
if nsfw {
let w_tags: Vec<&Tag> = event
.tags
.iter()
.filter(|el| el.kind() == TagKind::ContentWarning)
.collect();
!is_dup && w_tags.is_empty()
} else {
!is_dup
}
!is_dup
})
.cloned()
.collect()