Release v4.1 (#229)
* refactor: remove custom icon packs * fix: command not work on windows * fix: make open_window command async * feat: improve commands * feat: improve * refactor: column * feat: improve thread column * feat: improve * feat: add stories column * feat: improve * feat: add search column * feat: add reset password * feat: add subscription * refactor: settings * chore: improve commands * fix: crash on production * feat: use tauri store plugin for cache * feat: new icon * chore: update icon for windows * chore: improve some columns * chore: polish code
This commit is contained in:
@@ -4,14 +4,11 @@ use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||
use tauri::{Emitter, EventTarget, Manager, State};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
use tauri::{Emitter, Manager, State};
|
||||
|
||||
// #[cfg(target_os = "macos")]
|
||||
// use crate::commands::tray::create_tray_panel;
|
||||
use crate::{
|
||||
common::{get_user_settings, init_nip65, parse_event},
|
||||
Nostr, RichEvent, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT,
|
||||
common::{get_user_settings, init_nip65},
|
||||
Nostr, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT, NOTIFICATION_SUB_ID,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
@@ -165,6 +162,37 @@ pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result<Str
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn reset_password(key: String, password: String) -> Result<(), String> {
|
||||
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||
let keys = Keys::new(secret_key.clone());
|
||||
let npub = keys.public_key().to_bech32().unwrap();
|
||||
|
||||
let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||
|
||||
let keyring = Entry::new("Lume Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||
let account = Account {
|
||||
password: enc_bech32,
|
||||
nostr_connect: None,
|
||||
};
|
||||
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||
let _ = keyring.set_password(&j);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_private_key(id: String) -> Result<String, String> {
|
||||
let keyring = Entry::new("Lume Secret Storage", &id).map_err(|e| e.to_string())?;
|
||||
let password = keyring.get_password().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(password)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn delete_account(id: String) -> Result<(), String> {
|
||||
@@ -180,9 +208,8 @@ pub async fn login(
|
||||
account: String,
|
||||
password: String,
|
||||
state: State<'_, Nostr>,
|
||||
app: tauri::AppHandle,
|
||||
handle: tauri::AppHandle,
|
||||
) -> Result<String, String> {
|
||||
let handle = app.clone();
|
||||
let client = &state.client;
|
||||
let keyring = Entry::new("Lume Secret Storage", &account).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -229,77 +256,66 @@ pub async fn login(
|
||||
// Connect to user's relay (NIP-65)
|
||||
init_nip65(client).await;
|
||||
|
||||
// Create tray (macOS)
|
||||
// #[cfg(target_os = "macos")]
|
||||
// create_tray_panel(&public_key.to_bech32().unwrap(), &handle);
|
||||
|
||||
// Get user's contact list
|
||||
if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await {
|
||||
*state.contact_list.lock().unwrap() = contacts
|
||||
let mut contacts_state = state.contact_list.lock().await;
|
||||
*contacts_state = contacts;
|
||||
};
|
||||
|
||||
// Get user's settings
|
||||
if let Ok(settings) = get_user_settings(client).await {
|
||||
*state.settings.lock().unwrap() = settings
|
||||
let mut settings_state = state.settings.lock().await;
|
||||
*settings_state = settings;
|
||||
};
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let window = handle.get_window("main").unwrap();
|
||||
|
||||
let state = window.state::<Nostr>();
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
let contact_list = state.contact_list.lock().unwrap().clone();
|
||||
let contact_list = state.contact_list.lock().await;
|
||||
|
||||
let signer = client.signer().await.unwrap();
|
||||
let public_key = signer.public_key().await.unwrap();
|
||||
|
||||
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
||||
|
||||
if !contact_list.is_empty() {
|
||||
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
|
||||
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
||||
let sync = Filter::new()
|
||||
.authors(authors.clone())
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(NEWSFEED_NEG_LIMIT);
|
||||
|
||||
match client
|
||||
.reconcile(
|
||||
Filter::new()
|
||||
.authors(authors)
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(NEWSFEED_NEG_LIMIT),
|
||||
NegentropyOptions::default(),
|
||||
)
|
||||
if client
|
||||
.reconcile(sync, NegentropyOptions::default())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Ok(_) => {
|
||||
if handle.emit_to(EventTarget::Any, "synced", true).is_err() {
|
||||
println!("Emit event failed.")
|
||||
}
|
||||
}
|
||||
Err(_) => println!("Sync newsfeed failed."),
|
||||
handle.emit("newsfeed_synchronized", ()).unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
match client
|
||||
.reconcile(
|
||||
Filter::new()
|
||||
.pubkey(public_key)
|
||||
.kinds(vec![
|
||||
Kind::TextNote,
|
||||
Kind::Repost,
|
||||
Kind::Reaction,
|
||||
Kind::ZapReceipt,
|
||||
])
|
||||
.limit(NOTIFICATION_NEG_LIMIT),
|
||||
NegentropyOptions::default(),
|
||||
)
|
||||
drop(contact_list);
|
||||
|
||||
let sync = Filter::new()
|
||||
.pubkey(public_key)
|
||||
.kinds(vec![
|
||||
Kind::TextNote,
|
||||
Kind::Repost,
|
||||
Kind::Reaction,
|
||||
Kind::ZapReceipt,
|
||||
])
|
||||
.limit(NOTIFICATION_NEG_LIMIT);
|
||||
|
||||
// Sync notification with negentropy
|
||||
if client
|
||||
.reconcile(sync, NegentropyOptions::default())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Ok(_) => {
|
||||
if handle.emit_to(EventTarget::Any, "synced", true).is_err() {
|
||||
println!("Emit event failed.")
|
||||
}
|
||||
}
|
||||
Err(_) => println!("Sync notification failed."),
|
||||
};
|
||||
handle.emit("notification_synchronized", ()).unwrap();
|
||||
}
|
||||
|
||||
let subscription_id = SubscriptionId::new("notification");
|
||||
let subscription = Filter::new()
|
||||
let notification = Filter::new()
|
||||
.pubkey(public_key)
|
||||
.kinds(vec![
|
||||
Kind::TextNote,
|
||||
@@ -310,146 +326,12 @@ pub async fn login(
|
||||
.since(Timestamp::now());
|
||||
|
||||
// Subscribing for new notification...
|
||||
let _ = client
|
||||
.subscribe_with_id(subscription_id, vec![subscription], None)
|
||||
.await;
|
||||
|
||||
// Handle notifications
|
||||
client
|
||||
.handle_notifications(|notification| async {
|
||||
if let RelayPoolNotification::Message { message, .. } = notification {
|
||||
if let RelayMessage::Event {
|
||||
subscription_id,
|
||||
event,
|
||||
} = message
|
||||
{
|
||||
let id = subscription_id.to_string();
|
||||
|
||||
if id.starts_with("notification") {
|
||||
if app
|
||||
.emit_to(
|
||||
EventTarget::window("panel"),
|
||||
"notification",
|
||||
event.as_json(),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
println!("Emit new notification failed.")
|
||||
}
|
||||
|
||||
let handle = app.app_handle();
|
||||
let author = client.metadata(event.pubkey).await.unwrap();
|
||||
|
||||
match event.kind() {
|
||||
Kind::TextNote => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Mentioned you in a thread.")
|
||||
.title(
|
||||
author
|
||||
.display_name
|
||||
.unwrap_or_else(|| "Lume".to_string()),
|
||||
)
|
||||
.show()
|
||||
{
|
||||
println!("Failed to show notification: {:?}", e);
|
||||
}
|
||||
}
|
||||
Kind::Repost => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Reposted your note.")
|
||||
.title(
|
||||
author
|
||||
.display_name
|
||||
.unwrap_or_else(|| "Lume".to_string()),
|
||||
)
|
||||
.show()
|
||||
{
|
||||
println!("Failed to show notification: {:?}", e);
|
||||
}
|
||||
}
|
||||
Kind::Reaction => {
|
||||
let content = event.content();
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body(content)
|
||||
.title(
|
||||
author
|
||||
.display_name
|
||||
.unwrap_or_else(|| "Lume".to_string()),
|
||||
)
|
||||
.show()
|
||||
{
|
||||
println!("Failed to show notification: {:?}", e);
|
||||
}
|
||||
}
|
||||
Kind::ZapReceipt => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Zapped you.")
|
||||
.title(
|
||||
author
|
||||
.display_name
|
||||
.unwrap_or_else(|| "Lume".to_string()),
|
||||
)
|
||||
.show()
|
||||
{
|
||||
println!("Failed to show notification: {:?}", e);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if id.starts_with("event-") {
|
||||
let raw = event.as_json();
|
||||
let parsed = if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if app
|
||||
.emit_to(
|
||||
EventTarget::window(id),
|
||||
"new_reply",
|
||||
RichEvent { raw, parsed },
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
println!("Emit new notification failed.")
|
||||
}
|
||||
} else if id.starts_with("column-") {
|
||||
let raw = event.as_json();
|
||||
let parsed = if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if app
|
||||
.emit_to(
|
||||
EventTarget::window(id),
|
||||
"new_event",
|
||||
RichEvent { raw, parsed },
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
println!("Emit new notification failed.")
|
||||
}
|
||||
} else {
|
||||
println!("new event: {}", event.as_json())
|
||||
}
|
||||
} else {
|
||||
println!("new message: {}", message.as_json())
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
})
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id(notification_id, vec![notification], None)
|
||||
.await
|
||||
{
|
||||
println!("Error: {}", e)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(public_key)
|
||||
|
||||
@@ -5,7 +5,7 @@ use specta::Type;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use tauri::State;
|
||||
|
||||
use crate::common::{create_event_tags, dedup_event, parse_event, Meta};
|
||||
use crate::common::{create_event_tags, filter_converstation, parse_event, Meta};
|
||||
use crate::{Nostr, FETCH_LIMIT};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Type)]
|
||||
@@ -16,31 +16,21 @@ pub struct RichEvent {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_event_meta(content: &str) -> Result<Meta, ()> {
|
||||
let meta = parse_event(content).await;
|
||||
pub async fn get_event_meta(content: String) -> Result<Meta, ()> {
|
||||
let meta = parse_event(&content).await;
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<RichEvent, String> {
|
||||
pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent, String> {
|
||||
let client = &state.client;
|
||||
|
||||
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()),
|
||||
},
|
||||
};
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new().id(event_id);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![Filter::new().id(event_id)],
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
@@ -66,30 +56,49 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<RichEvent, S
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_event_from(
|
||||
id: &str,
|
||||
relay_hint: &str,
|
||||
id: String,
|
||||
relay_hint: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<RichEvent, String> {
|
||||
let client = &state.client;
|
||||
let settings = state
|
||||
.settings
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.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()),
|
||||
},
|
||||
};
|
||||
let settings = state.settings.lock().await;
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new().id(event_id);
|
||||
|
||||
if !settings.use_relay_hint {
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.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()),
|
||||
}
|
||||
} else {
|
||||
// Add relay hint to relay pool
|
||||
if let Err(e) = client.add_relay(&relay_hint).await {
|
||||
return Err(e.to_string());
|
||||
}
|
||||
|
||||
if let Err(e) = client.connect_relay(&relay_hint).await {
|
||||
return Err(e.to_string());
|
||||
}
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![Filter::new().id(event_id)],
|
||||
@@ -113,49 +122,14 @@ pub async fn get_event_from(
|
||||
}
|
||||
Err(err) => 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_from(vec![relay_hint], 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()),
|
||||
}
|
||||
} else {
|
||||
Err("Relay connection failed.".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
|
||||
pub async fn get_replies(id: String, state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
|
||||
let event_id = match EventId::from_hex(id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return Err(err.to_string()),
|
||||
};
|
||||
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
|
||||
|
||||
match client
|
||||
@@ -166,7 +140,7 @@ pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result<Vec<RichEv
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let futures = events.into_iter().map(|ev| async move {
|
||||
let futures = events.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
@@ -186,73 +160,63 @@ pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result<Vec<RichEv
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn listen_event_reply(id: &str, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
pub async fn subscribe_to(id: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
|
||||
let mut label = "event-".to_owned();
|
||||
label.push_str(id);
|
||||
let subscription_id = SubscriptionId::new(&id);
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
|
||||
let sub_id = SubscriptionId::new(label);
|
||||
let event_id = match EventId::from_hex(id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return Err(err.to_string()),
|
||||
};
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote])
|
||||
.event(event_id)
|
||||
.since(Timestamp::now());
|
||||
|
||||
// Subscribe
|
||||
let _ = client.subscribe_with_id(sub_id, vec![filter], None).await;
|
||||
|
||||
Ok(())
|
||||
match client
|
||||
.subscribe_with_id(subscription_id, vec![filter], None)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_events_by(
|
||||
public_key: &str,
|
||||
as_of: Option<&str>,
|
||||
public_key: String,
|
||||
limit: i32,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
let author = PublicKey::parse(&public_key).map_err(|err| err.to_string())?;
|
||||
|
||||
match PublicKey::from_str(public_key) {
|
||||
Ok(author) => {
|
||||
let until = match as_of {
|
||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.author(author)
|
||||
.limit(FETCH_LIMIT)
|
||||
.until(until);
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote])
|
||||
.author(author)
|
||||
.limit(limit as usize);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let futures = events.into_iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let fils = filter_converstation(events);
|
||||
let futures = fils.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
RichEvent { raw, parsed }
|
||||
});
|
||||
let rich_events = join_all(futures).await;
|
||||
RichEvent { raw, parsed }
|
||||
});
|
||||
let rich_events = join_all(futures).await;
|
||||
|
||||
Ok(rich_events)
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
Ok(rich_events)
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
@@ -260,34 +224,33 @@ pub async fn get_events_by(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_local_events(
|
||||
pub async fn get_events_from_contacts(
|
||||
until: Option<&str>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
let contact_list = state
|
||||
.contact_list
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.clone();
|
||||
let contact_list = state.contact_list.lock().await;
|
||||
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
||||
|
||||
if authors.is_empty() {
|
||||
return Err("Contact List is empty.".into());
|
||||
}
|
||||
|
||||
let as_of = match until {
|
||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(64)
|
||||
.limit(FETCH_LIMIT)
|
||||
.until(as_of)
|
||||
.authors(authors);
|
||||
|
||||
match client.database().query(vec![filter], Order::Desc).await {
|
||||
Ok(events) => {
|
||||
let dedup = dedup_event(&events);
|
||||
let futures = dedup.into_iter().map(|ev| async move {
|
||||
let fils = filter_converstation(events);
|
||||
let futures = fils.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
@@ -305,31 +268,6 @@ pub async fn get_local_events(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn listen_local_event(label: &str, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
|
||||
let contact_list = state
|
||||
.contact_list
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.clone();
|
||||
|
||||
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
|
||||
let sub_id = SubscriptionId::new(label);
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.authors(authors)
|
||||
.since(Timestamp::now());
|
||||
|
||||
// Subscribe
|
||||
let _ = client.subscribe_with_id(sub_id, vec![filter], None).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_group_events(
|
||||
@@ -345,7 +283,7 @@ pub async fn get_group_events(
|
||||
};
|
||||
|
||||
let authors: Vec<PublicKey> = public_keys
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|p| {
|
||||
if p.starts_with("npub1") {
|
||||
PublicKey::from_bech32(p).map_err(|err| err.to_string())
|
||||
@@ -369,9 +307,8 @@ pub async fn get_group_events(
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let dedup = dedup_event(&events);
|
||||
|
||||
let futures = dedup.into_iter().map(|ev| async move {
|
||||
let fils = filter_converstation(events);
|
||||
let futures = fils.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
@@ -415,8 +352,8 @@ pub async fn get_global_events(
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let dedup = dedup_event(&events);
|
||||
let futures = dedup.into_iter().map(|ev| async move {
|
||||
let fils = filter_converstation(events);
|
||||
let futures = fils.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
@@ -460,8 +397,8 @@ pub async fn get_hashtag_events(
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let dedup = dedup_event(&events);
|
||||
let futures = dedup.into_iter().map(|ev| async move {
|
||||
let fils = filter_converstation(events);
|
||||
let futures = fils.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
@@ -540,17 +477,14 @@ pub async fn reply(
|
||||
// Create tags from content
|
||||
let mut tags = create_event_tags(&content);
|
||||
|
||||
let reply_id = match EventId::from_hex(to) {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Event is not valid.".into()),
|
||||
};
|
||||
let reply_id = EventId::parse(&to).map_err(|err| err.to_string())?;
|
||||
|
||||
match database
|
||||
.query(vec![Filter::new().id(reply_id)], Order::Desc)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
if let Some(event) = events.first() {
|
||||
let relay_hint = if let Some(relays) = database
|
||||
.event_seen_on_relays(event.id)
|
||||
.await
|
||||
@@ -585,7 +519,7 @@ pub async fn reply(
|
||||
.query(vec![Filter::new().id(root_id)], Order::Desc)
|
||||
.await
|
||||
{
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
if let Some(event) = events.first() {
|
||||
let relay_hint = if let Some(relays) = database
|
||||
.event_seen_on_relays(event.id)
|
||||
.await
|
||||
@@ -627,13 +561,21 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<String, String
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn event_to_bech32(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
pub async fn delete(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
|
||||
let event_id = match EventId::from_hex(id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return Err("ID is not valid.".into()),
|
||||
};
|
||||
match client.delete_event(event_id).await {
|
||||
Ok(event_id) => Ok(event_id.to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn event_to_bech32(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
|
||||
let seens = client
|
||||
.database()
|
||||
@@ -662,11 +604,7 @@ pub async fn event_to_bech32(id: &str, state: State<'_, Nostr>) -> Result<String
|
||||
#[specta::specta]
|
||||
pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
|
||||
let public_key = match PublicKey::from_str(user) {
|
||||
Ok(pk) => pk,
|
||||
Err(_) => return Err("Public Key is not valid.".into()),
|
||||
};
|
||||
let public_key = PublicKey::parse(user).map_err(|err| err.to_string())?;
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
@@ -704,12 +642,48 @@ pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result<Strin
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn unlisten(id: &str, state: State<'_, Nostr>) -> Result<(), ()> {
|
||||
pub async fn search(
|
||||
query: String,
|
||||
until: Option<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
let sub_id = SubscriptionId::new(id);
|
||||
|
||||
// Remove subscription
|
||||
client.unsubscribe(sub_id).await;
|
||||
let timestamp = match until {
|
||||
Some(str) => Timestamp::from_str(&str).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Metadata])
|
||||
.search(query)
|
||||
.until(timestamp)
|
||||
.limit(FETCH_LIMIT);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let fils = filter_converstation(events);
|
||||
let futures = fils.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
RichEvent { raw, parsed }
|
||||
});
|
||||
|
||||
let rich_events = join_all(futures).await;
|
||||
|
||||
Ok(rich_events)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
use keyring::Entry;
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use tauri::State;
|
||||
use tauri_specta::Event;
|
||||
|
||||
use crate::{Nostr, Settings};
|
||||
use crate::{NewSettings, Nostr, Settings};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||
pub struct Profile {
|
||||
name: String,
|
||||
display_name: String,
|
||||
about: Option<String>,
|
||||
picture: String,
|
||||
banner: Option<String>,
|
||||
nip05: Option<String>,
|
||||
lud16: Option<String>,
|
||||
website: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let public_key: PublicKey = match id {
|
||||
Some(user_id) => match PublicKey::from_str(&user_id) {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Public Key is not valid".into()),
|
||||
},
|
||||
Some(user_id) => PublicKey::parse(&user_id).map_err(|e| e.to_string())?,
|
||||
None => client.signer().await.unwrap().public_key().await.unwrap(),
|
||||
};
|
||||
|
||||
@@ -22,43 +34,46 @@ pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<
|
||||
.kind(Kind::Metadata)
|
||||
.limit(1);
|
||||
|
||||
let query = client
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(3))),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(events) = query {
|
||||
if let Some(event) = events.first() {
|
||||
if let Ok(metadata) = Metadata::from_json(&event.content) {
|
||||
Ok(metadata.as_json())
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
if let Ok(metadata) = Metadata::from_json(&event.content) {
|
||||
Ok(metadata.as_json())
|
||||
} else {
|
||||
Err("Parse metadata failed".into())
|
||||
}
|
||||
} else {
|
||||
Err("Parse metadata failed".into())
|
||||
Ok(Metadata::new().as_json())
|
||||
}
|
||||
} else {
|
||||
Ok(Metadata::new().as_json())
|
||||
}
|
||||
} else {
|
||||
Err("Get metadata failed".into())
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_contact_list(
|
||||
public_keys: Vec<&str>,
|
||||
public_keys: Vec<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let contact_list: Vec<Contact> = public_keys
|
||||
.into_iter()
|
||||
.filter_map(|p| match PublicKey::from_hex(p) {
|
||||
.filter_map(|p| match PublicKey::parse(p) {
|
||||
Ok(pk) => Some(Contact::new(pk, None, Some(""))),
|
||||
Err(_) => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Update local state
|
||||
state.contact_list.lock().await.clone_from(&contact_list);
|
||||
|
||||
match client.set_contact_list(contact_list).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(err) => Err(err.to_string()),
|
||||
@@ -89,77 +104,69 @@ pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, St
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn create_profile(
|
||||
name: &str,
|
||||
display_name: &str,
|
||||
about: &str,
|
||||
picture: &str,
|
||||
banner: &str,
|
||||
nip05: &str,
|
||||
lud16: &str,
|
||||
website: &str,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<String, String> {
|
||||
pub async fn set_profile(profile: Profile, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let mut metadata = Metadata::new()
|
||||
.name(name)
|
||||
.display_name(display_name)
|
||||
.about(about)
|
||||
.nip05(nip05)
|
||||
.lud16(lud16);
|
||||
.name(profile.name)
|
||||
.display_name(profile.display_name)
|
||||
.about(profile.about.unwrap_or_default())
|
||||
.nip05(profile.nip05.unwrap_or_default())
|
||||
.lud16(profile.lud16.unwrap_or_default());
|
||||
|
||||
if let Ok(url) = Url::parse(picture) {
|
||||
if let Ok(url) = Url::parse(&profile.picture) {
|
||||
metadata = metadata.picture(url)
|
||||
}
|
||||
|
||||
if let Ok(url) = Url::parse(banner) {
|
||||
metadata = metadata.banner(url)
|
||||
if let Some(b) = profile.banner {
|
||||
if let Ok(url) = Url::parse(&b) {
|
||||
metadata = metadata.banner(url)
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(url) = Url::parse(website) {
|
||||
metadata = metadata.website(url)
|
||||
if let Some(w) = profile.website {
|
||||
if let Ok(url) = Url::parse(&w) {
|
||||
metadata = metadata.website(url)
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(event_id) = client.set_metadata(&metadata).await {
|
||||
Ok(event_id.to_string())
|
||||
} else {
|
||||
Err("Create profile failed".into())
|
||||
match client.set_metadata(&metadata).await {
|
||||
Ok(id) => Ok(id.to_string()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn is_contact_list_empty(state: State<'_, Nostr>) -> Result<bool, ()> {
|
||||
let contact_list = state.contact_list.lock().unwrap();
|
||||
Ok(contact_list.is_empty())
|
||||
Ok(state.contact_list.lock().await.is_empty())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn check_contact(hex: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let contact_list = state.contact_list.lock().unwrap();
|
||||
let contact_list = state.contact_list.lock().await;
|
||||
|
||||
match PublicKey::from_str(&hex) {
|
||||
match PublicKey::parse(&hex) {
|
||||
Ok(public_key) => match contact_list.iter().position(|x| x.public_key == public_key) {
|
||||
Some(_) => Ok(true),
|
||||
None => Ok(false),
|
||||
},
|
||||
Err(err) => Err(err.to_string()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn toggle_contact(
|
||||
hex: &str,
|
||||
alias: Option<&str>,
|
||||
id: String,
|
||||
alias: Option<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
|
||||
match client.get_contact_list(None).await {
|
||||
match client.get_contact_list(Some(Duration::from_secs(5))).await {
|
||||
Ok(mut contact_list) => {
|
||||
let public_key = PublicKey::from_str(hex).unwrap();
|
||||
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
match contact_list.iter().position(|x| x.public_key == public_key) {
|
||||
Some(index) => {
|
||||
@@ -175,7 +182,7 @@ pub async fn toggle_contact(
|
||||
}
|
||||
|
||||
// Update local state
|
||||
state.contact_list.lock().unwrap().clone_from(&contact_list);
|
||||
state.contact_list.lock().await.clone_from(&contact_list);
|
||||
|
||||
// Publish
|
||||
match client.set_contact_list(contact_list).await {
|
||||
@@ -189,9 +196,9 @@ pub async fn toggle_contact(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_nstore(
|
||||
key: &str,
|
||||
content: &str,
|
||||
pub async fn set_lume_store(
|
||||
key: String,
|
||||
content: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
@@ -202,7 +209,6 @@ pub async fn set_nstore(
|
||||
.nip44_encrypt(public_key, content)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let tag = Tag::identifier(key);
|
||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]);
|
||||
|
||||
@@ -214,7 +220,7 @@ pub async fn set_nstore(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
pub async fn get_lume_store(key: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||
@@ -226,16 +232,12 @@ pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, St
|
||||
.limit(1);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.get_events_of(vec![filter], EventSource::Database)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
let content = event.content();
|
||||
match signer.nip44_decrypt(public_key, content).await {
|
||||
match signer.nip44_decrypt(public_key, event.content()).await {
|
||||
Ok(decrypted) => Ok(decrypted),
|
||||
Err(_) => Err(event.content.to_string()),
|
||||
}
|
||||
@@ -316,7 +318,7 @@ pub async fn zap_profile(
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let public_key: PublicKey = PublicKey::from_str(id).map_err(|e| e.to_string())?;
|
||||
let public_key: PublicKey = PublicKey::parse(id).map_err(|e| e.to_string())?;
|
||||
|
||||
let details = ZapDetails::new(ZapType::Private).message(message);
|
||||
let num = amount.parse::<u64>().map_err(|e| e.to_string())?;
|
||||
@@ -361,7 +363,7 @@ pub async fn zap_event(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
pub async fn copy_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
|
||||
match PublicKey::from_bech32(npub) {
|
||||
@@ -408,7 +410,7 @@ pub async fn get_following(
|
||||
public_key: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::from_str(public_key).map_err(|e| e.to_string())?;
|
||||
let public_key = PublicKey::parse(public_key).map_err(|e| e.to_string())?;
|
||||
|
||||
let filter = Filter::new().kind(Kind::ContactList).author(public_key);
|
||||
let events = match client
|
||||
@@ -444,7 +446,7 @@ pub async fn get_followers(
|
||||
public_key: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::from_str(public_key).map_err(|e| e.to_string())?;
|
||||
let public_key = PublicKey::parse(public_key).map_err(|e| e.to_string())?;
|
||||
|
||||
let filter = Filter::new().kind(Kind::ContactList).custom_tag(
|
||||
SingleLetterTag::lowercase(Alphabet::P),
|
||||
@@ -505,28 +507,51 @@ pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, S
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_settings(state: State<'_, Nostr>) -> Result<Settings, ()> {
|
||||
let settings = state.settings.lock().unwrap().clone();
|
||||
Ok(settings)
|
||||
Ok(state.settings.lock().await.clone())
|
||||
}
|
||||
|
||||
#[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;
|
||||
pub async fn set_settings(
|
||||
settings: &str,
|
||||
state: State<'_, Nostr>,
|
||||
handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let ident = "lume_v4:settings";
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||
let encrypted = signer
|
||||
.nip44_encrypt(public_key, settings)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let tag = Tag::identifier(ident);
|
||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
match client.send_event_builder(builder).await {
|
||||
Ok(_) => {
|
||||
let parsed: Settings = serde_json::from_str(settings).map_err(|e| e.to_string())?;
|
||||
|
||||
#[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())
|
||||
// Update state
|
||||
state.settings.lock().await.clone_from(&parsed);
|
||||
|
||||
// Emit new changes to frontend
|
||||
NewSettings(parsed).emit(&handle).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn verify_nip05(id: String, nip05: &str) -> Result<bool, String> {
|
||||
match PublicKey::from_str(&id) {
|
||||
Ok(public_key) => match nip05::verify(&public_key, nip05, None).await {
|
||||
Ok(status) => Ok(status),
|
||||
Err(e) => Err(e.to_string()),
|
||||
},
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,4 @@ pub mod account;
|
||||
pub mod event;
|
||||
pub mod metadata;
|
||||
pub mod relay;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod tray;
|
||||
pub mod window;
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use tauri::window::{Effect, EffectsBuilder};
|
||||
use tauri::{
|
||||
tray::{MouseButtonState, TrayIconEvent},
|
||||
WebviewWindowBuilder,
|
||||
};
|
||||
use tauri::{AppHandle, Manager, WebviewUrl};
|
||||
use tauri_nspanel::ManagerExt;
|
||||
|
||||
use crate::macos::{
|
||||
position_menubar_panel, set_corner_radius, setup_menubar_panel_listeners,
|
||||
swizzle_to_menubar_panel,
|
||||
};
|
||||
|
||||
pub fn create_tray_panel(account: &str, app: &AppHandle) {
|
||||
let tray = app.tray_by_id("main").unwrap();
|
||||
|
||||
tray.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click { button_state, .. } = event {
|
||||
if button_state == MouseButtonState::Up {
|
||||
let app = tray.app_handle();
|
||||
let panel = app.get_webview_panel("panel").unwrap();
|
||||
|
||||
match panel.is_visible() {
|
||||
true => panel.order_out(None),
|
||||
false => {
|
||||
position_menubar_panel(app, 0.0);
|
||||
panel.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(window) = app.get_webview_window("panel") {
|
||||
let _ = window.destroy();
|
||||
};
|
||||
|
||||
let url = format!("/{}/panel", account);
|
||||
|
||||
let window = WebviewWindowBuilder::new(app, "panel", WebviewUrl::App(PathBuf::from(url)))
|
||||
.title("Panel")
|
||||
.inner_size(350.0, 500.0)
|
||||
.fullscreen(false)
|
||||
.resizable(false)
|
||||
.visible(false)
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let _ = window.set_effects(
|
||||
EffectsBuilder::new()
|
||||
.effect(Effect::Popover)
|
||||
.state(tauri::window::EffectState::FollowsWindowActiveState)
|
||||
.build(),
|
||||
);
|
||||
|
||||
set_corner_radius(&window, 13.0);
|
||||
|
||||
// Convert window to panel
|
||||
swizzle_to_menubar_panel(app);
|
||||
setup_menubar_panel_listeners(app);
|
||||
}
|
||||
@@ -8,9 +8,8 @@ use tauri::utils::config::WindowEffectsConfig;
|
||||
use tauri::window::Effect;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::WebviewWindowBuilder;
|
||||
use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl};
|
||||
#[cfg(target_os = "windows")]
|
||||
use tauri::{WebviewBuilder, WebviewWindowBuilder};
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
|
||||
#[derive(Serialize, Deserialize, Type)]
|
||||
@@ -45,7 +44,7 @@ pub fn create_column(column: Column, app_handle: tauri::AppHandle) -> Result<Str
|
||||
let path = PathBuf::from(column.url);
|
||||
let webview_url = WebviewUrl::App(path);
|
||||
|
||||
let builder = tauri::webview::WebviewBuilder::new(column.label, webview_url)
|
||||
let builder = WebviewBuilder::new(column.label, webview_url)
|
||||
.incognito(true)
|
||||
.transparent(true);
|
||||
|
||||
@@ -68,14 +67,8 @@ pub fn create_column(column: Column, app_handle: tauri::AppHandle) -> Result<Str
|
||||
#[specta::specta]
|
||||
pub fn close_column(label: String, app_handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
match app_handle.get_webview(&label) {
|
||||
Some(webview) => {
|
||||
if webview.close().is_ok() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
None => Err("Column not found.".into()),
|
||||
Some(webview) => Ok(webview.close().is_ok()),
|
||||
None => Err("Not found.".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,16 +79,10 @@ pub fn reposition_column(
|
||||
x: f32,
|
||||
y: f32,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<bool, String> {
|
||||
match app_handle.get_webview(&label) {
|
||||
Some(webview) => {
|
||||
if webview.set_position(LogicalPosition::new(x, y)).is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Reposition column failed".into())
|
||||
}
|
||||
}
|
||||
None => Err("Webview not found".into()),
|
||||
Some(webview) => Ok(webview.set_position(LogicalPosition::new(x, y)).is_ok()),
|
||||
None => Err("Not found".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,36 +93,25 @@ pub fn resize_column(
|
||||
width: f32,
|
||||
height: f32,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<bool, String> {
|
||||
match app_handle.get_webview(&label) {
|
||||
Some(webview) => {
|
||||
if webview.set_size(LogicalSize::new(width, height)).is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Resize column failed".into())
|
||||
}
|
||||
}
|
||||
None => Err("Webview not found".into()),
|
||||
Some(webview) => Ok(webview.set_size(LogicalSize::new(width, height)).is_ok()),
|
||||
None => Err("Not found".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
match app_handle.get_webview(&label) {
|
||||
Some(webview) => {
|
||||
if webview.eval("window.location.reload()").is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Reload column failed".into())
|
||||
}
|
||||
}
|
||||
None => Err("Webview not found".into()),
|
||||
Some(webview) => Ok(webview.eval("window.location.reload()").is_ok()),
|
||||
None => Err("Not found".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app_handle.get_window(&window.label) {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
@@ -145,7 +121,6 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
let _ = window.set_focus();
|
||||
};
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
let window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&window.label,
|
||||
@@ -168,7 +143,25 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
// Restore native border
|
||||
window.add_border(None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app_handle.get_window(&window.label) {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
};
|
||||
} else {
|
||||
let window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&window.label,
|
||||
@@ -180,6 +173,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.minimizable(window.minimizable)
|
||||
.maximizable(window.maximizable)
|
||||
.transparent(true)
|
||||
.decorations(false)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::Mica],
|
||||
@@ -190,12 +184,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.unwrap();
|
||||
|
||||
// Set decoration
|
||||
#[cfg(target_os = "windows")]
|
||||
window.create_overlay_titlebar().unwrap();
|
||||
|
||||
// Restore native border
|
||||
#[cfg(target_os = "macos")]
|
||||
window.add_border(None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -203,7 +192,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn open_main_window(app: tauri::AppHandle) {
|
||||
pub fn reopen_lume(app: tauri::AppHandle) {
|
||||
if let Some(window) = app.get_window("main") {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
@@ -225,11 +214,25 @@ pub fn open_main_window(app: tauri::AppHandle) {
|
||||
// Restore native border
|
||||
#[cfg(target_os = "macos")]
|
||||
window.add_border(None);
|
||||
|
||||
// Set a custom inset to the traffic lights
|
||||
#[cfg(target_os = "macos")]
|
||||
window.set_traffic_lights_inset(7.0, 13.0).unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let win = window.clone();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::ThemeChanged(_) = event {
|
||||
win.set_traffic_lights_inset(7.0, 13.0).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn force_quit() {
|
||||
pub fn quit() {
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
@@ -46,106 +46,27 @@ const NOSTR_MENTIONS: [&str; 10] = [
|
||||
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
||||
const VIDEOS: [&str; 5] = ["mp4", "mov", "avi", "webm", "mkv"];
|
||||
|
||||
pub async fn init_nip65(client: &Client) {
|
||||
let signer = match client.signer().await {
|
||||
Ok(signer) => signer,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get signer: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let public_key = match signer.public_key().await {
|
||||
Ok(public_key) => public_key,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get public key: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::RelayList)
|
||||
.limit(1);
|
||||
|
||||
if let Ok(events) = client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some(event) = events.first() {
|
||||
let relay_list = nip65::extract_relay_list(event);
|
||||
for (url, metadata) in relay_list {
|
||||
let opts = match metadata {
|
||||
Some(RelayMetadata::Read) => RelayOptions::new().read(true).write(false),
|
||||
Some(_) => RelayOptions::new().write(true).read(false),
|
||||
None => RelayOptions::default(),
|
||||
};
|
||||
if let Err(e) = client.add_relay_with_opts(&url.to_string(), opts).await {
|
||||
eprintln!("Failed to add relay {}: {:?}", url, e);
|
||||
}
|
||||
if let Err(e) = client.connect_relay(url.to_string()).await {
|
||||
eprintln!("Failed to connect to relay {}: {:?}", url, e);
|
||||
} else {
|
||||
println!("Connecting to relay: {} - {:?}", url, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Failed to get events for RelayList.");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_user_settings(client: &Client) -> Result<Settings, String> {
|
||||
let ident = "lume:settings";
|
||||
let signer = client
|
||||
.signer()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get signer: {:?}", e))?;
|
||||
let public_key = signer
|
||||
.public_key()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get public key: {:?}", e))?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ident)
|
||||
.limit(1);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
let content = event.content();
|
||||
match signer.nip44_decrypt(public_key, content).await {
|
||||
Ok(decrypted) => match serde_json::from_str(&decrypted) {
|
||||
Ok(parsed) => Ok(parsed),
|
||||
Err(_) => Err("Could not parse settings payload".into()),
|
||||
},
|
||||
Err(e) => Err(format!("Failed to decrypt settings content: {:?}", e)),
|
||||
}
|
||||
} else {
|
||||
Err("Settings not found.".into())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(format!(
|
||||
"Failed to get events for ApplicationSpecificData: {:?}",
|
||||
e
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
|
||||
events.iter().next()
|
||||
}
|
||||
|
||||
pub fn filter_converstation(events: Vec<Event>) -> Vec<Event> {
|
||||
events
|
||||
.into_iter()
|
||||
.filter_map(|ev| {
|
||||
let tags = ev.get_tags_content(TagKind::SingleLetter(SingleLetterTag::lowercase(
|
||||
Alphabet::E,
|
||||
)));
|
||||
|
||||
if tags.is_empty() {
|
||||
Some(ev)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Event>>()
|
||||
}
|
||||
|
||||
pub fn dedup_event(events: &[Event]) -> Vec<Event> {
|
||||
let mut seen_ids = HashSet::new();
|
||||
events
|
||||
@@ -331,6 +252,102 @@ pub fn create_event_tags(content: &str) -> Vec<Tag> {
|
||||
tags
|
||||
}
|
||||
|
||||
pub async fn init_nip65(client: &Client) {
|
||||
let signer = match client.signer().await {
|
||||
Ok(signer) => signer,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get signer: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let public_key = match signer.public_key().await {
|
||||
Ok(public_key) => public_key,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get public key: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::RelayList)
|
||||
.limit(1);
|
||||
|
||||
if let Ok(events) = client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some(event) = events.first() {
|
||||
let relay_list = nip65::extract_relay_list(event);
|
||||
for (url, metadata) in relay_list {
|
||||
let opts = match metadata {
|
||||
Some(RelayMetadata::Read) => RelayOptions::new().read(true).write(false),
|
||||
Some(_) => RelayOptions::new().write(true).read(false),
|
||||
None => RelayOptions::default(),
|
||||
};
|
||||
if let Err(e) = client.add_relay_with_opts(&url.to_string(), opts).await {
|
||||
eprintln!("Failed to add relay {}: {:?}", url, e);
|
||||
}
|
||||
if let Err(e) = client.connect_relay(url.to_string()).await {
|
||||
eprintln!("Failed to connect to relay {}: {:?}", url, e);
|
||||
} else {
|
||||
println!("Connecting to relay: {} - {:?}", url, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Failed to get events for RelayList.");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_user_settings(client: &Client) -> Result<Settings, String> {
|
||||
let ident = "lume_v4:settings";
|
||||
let signer = client
|
||||
.signer()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get signer: {:?}", e))?;
|
||||
let public_key = signer
|
||||
.public_key()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get public key: {:?}", e))?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ident)
|
||||
.limit(1);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
let content = event.content();
|
||||
match signer.nip44_decrypt(public_key, content).await {
|
||||
Ok(decrypted) => match serde_json::from_str(&decrypted) {
|
||||
Ok(parsed) => Ok(parsed),
|
||||
Err(_) => Err("Could not parse settings payload".into()),
|
||||
},
|
||||
Err(e) => Err(format!("Failed to decrypt settings content: {:?}", e)),
|
||||
}
|
||||
} else {
|
||||
Err("Settings not found.".into())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(format!(
|
||||
"Failed to get events for ApplicationSpecificData: {:?}",
|
||||
e
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
use std::ffi::CString;
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, WebviewWindow};
|
||||
use tauri_nspanel::{
|
||||
block::ConcreteBlock,
|
||||
cocoa::{
|
||||
appkit::{NSMainMenuWindowLevel, NSView, NSWindow, NSWindowCollectionBehavior},
|
||||
base::{id, nil},
|
||||
foundation::{NSPoint, NSRect},
|
||||
},
|
||||
objc::{class, msg_send, runtime::NO, sel, sel_impl},
|
||||
panel_delegate, ManagerExt, WebviewWindowExt,
|
||||
};
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;
|
||||
|
||||
pub fn swizzle_to_menubar_panel(app_handle: &tauri::AppHandle) {
|
||||
let panel_delegate = panel_delegate!(SpotlightPanelDelegate {
|
||||
window_did_resign_key
|
||||
});
|
||||
|
||||
let window = app_handle.get_webview_window("panel").unwrap();
|
||||
|
||||
let panel = window.to_panel().unwrap();
|
||||
|
||||
let handle = app_handle.clone();
|
||||
|
||||
panel_delegate.set_listener(Box::new(move |delegate_name: String| {
|
||||
if delegate_name.as_str() == "window_did_resign_key" {
|
||||
let _ = handle.emit("menubar_panel_did_resign_key", ());
|
||||
}
|
||||
}));
|
||||
|
||||
panel.set_level(NSMainMenuWindowLevel + 1);
|
||||
|
||||
panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel);
|
||||
|
||||
panel.set_collection_behaviour(
|
||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces
|
||||
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary
|
||||
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
|
||||
);
|
||||
|
||||
panel.set_delegate(panel_delegate);
|
||||
}
|
||||
|
||||
pub fn setup_menubar_panel_listeners(app_handle: &AppHandle) {
|
||||
fn hide_menubar_panel(app_handle: &tauri::AppHandle) {
|
||||
if check_menubar_frontmost() {
|
||||
return;
|
||||
}
|
||||
|
||||
let panel = app_handle.get_webview_panel("panel").unwrap();
|
||||
|
||||
panel.order_out(None);
|
||||
}
|
||||
|
||||
let handle = app_handle.clone();
|
||||
|
||||
app_handle.listen_any("menubar_panel_did_resign_key", move |_| {
|
||||
hide_menubar_panel(&handle);
|
||||
});
|
||||
|
||||
let handle = app_handle.clone();
|
||||
|
||||
let callback = Box::new(move || {
|
||||
hide_menubar_panel(&handle);
|
||||
});
|
||||
|
||||
register_workspace_listener(
|
||||
"NSWorkspaceDidActivateApplicationNotification".into(),
|
||||
callback.clone(),
|
||||
);
|
||||
|
||||
register_workspace_listener(
|
||||
"NSWorkspaceActiveSpaceDidChangeNotification".into(),
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_corner_radius(window: &WebviewWindow, radius: f64) {
|
||||
let win: id = window.ns_window().unwrap() as _;
|
||||
|
||||
unsafe {
|
||||
let view: id = win.contentView();
|
||||
|
||||
view.wantsLayer();
|
||||
|
||||
let layer: id = view.layer();
|
||||
|
||||
let _: () = msg_send![layer, setCornerRadius: radius];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position_menubar_panel(app_handle: &tauri::AppHandle, padding_top: f64) {
|
||||
let window = app_handle.get_webview_window("panel").unwrap();
|
||||
|
||||
let monitor = monitor::get_monitor_with_cursor().unwrap();
|
||||
|
||||
let scale_factor = monitor.scale_factor();
|
||||
|
||||
let visible_area = monitor.visible_area();
|
||||
|
||||
let monitor_pos = visible_area.position().to_logical::<f64>(scale_factor);
|
||||
|
||||
let monitor_size = visible_area.size().to_logical::<f64>(scale_factor);
|
||||
|
||||
let mouse_location: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] };
|
||||
|
||||
let handle: id = window.ns_window().unwrap() as _;
|
||||
|
||||
let mut win_frame: NSRect = unsafe { msg_send![handle, frame] };
|
||||
|
||||
win_frame.origin.y = (monitor_pos.y + monitor_size.height) - win_frame.size.height;
|
||||
|
||||
win_frame.origin.y -= padding_top;
|
||||
|
||||
win_frame.origin.x = {
|
||||
let top_right = mouse_location.x + (win_frame.size.width / 2.0);
|
||||
|
||||
let is_offscreen = top_right > monitor_pos.x + monitor_size.width;
|
||||
|
||||
if !is_offscreen {
|
||||
mouse_location.x - (win_frame.size.width / 2.0)
|
||||
} else {
|
||||
let diff = top_right - (monitor_pos.x + monitor_size.width);
|
||||
|
||||
mouse_location.x - (win_frame.size.width / 2.0) - diff
|
||||
}
|
||||
};
|
||||
|
||||
let _: () = unsafe { msg_send![handle, setFrame: win_frame display: NO] };
|
||||
}
|
||||
|
||||
fn register_workspace_listener(name: String, callback: Box<dyn Fn()>) {
|
||||
let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] };
|
||||
let notification_center: id = unsafe { msg_send![workspace, notificationCenter] };
|
||||
|
||||
let block = ConcreteBlock::new(move |_notif: id| {
|
||||
callback();
|
||||
});
|
||||
|
||||
let block = block.copy();
|
||||
|
||||
let name: id =
|
||||
unsafe { msg_send![class!(NSString), stringWithCString: CString::new(name).unwrap()] };
|
||||
|
||||
unsafe {
|
||||
let _: () = msg_send![
|
||||
notification_center,
|
||||
addObserverForName: name object: nil queue: nil usingBlock: block
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
fn app_pid() -> i32 {
|
||||
let process_info: id = unsafe { msg_send![class!(NSProcessInfo), processInfo] };
|
||||
let pid: i32 = unsafe { msg_send![process_info, processIdentifier] };
|
||||
|
||||
pid
|
||||
}
|
||||
|
||||
fn get_frontmost_app_pid() -> i32 {
|
||||
let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] };
|
||||
let frontmost_application: id = unsafe { msg_send![workspace, frontmostApplication] };
|
||||
let pid: i32 = unsafe { msg_send![frontmost_application, processIdentifier] };
|
||||
|
||||
pid
|
||||
}
|
||||
|
||||
pub fn check_menubar_frontmost() -> bool {
|
||||
get_frontmost_app_pid() == app_pid()
|
||||
}
|
||||
@@ -3,12 +3,10 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
extern crate cocoa;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use border::WebviewWindowExt as BorderWebviewWindowExt;
|
||||
use commands::{account::*, event::*, metadata::*, relay::*, window::*};
|
||||
use common::parse_event;
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
@@ -17,21 +15,18 @@ use std::{
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
str::FromStr,
|
||||
sync::Mutex,
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::{path::BaseDirectory, Manager};
|
||||
use tauri::{path::BaseDirectory, Emitter, EventTarget, Manager};
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
use tauri_specta::{collect_commands, Builder};
|
||||
use tauri_plugin_notification::{NotificationExt, PermissionState};
|
||||
use tauri_specta::{collect_commands, collect_events, Builder, Event as TauriEvent};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub mod commands;
|
||||
pub mod common;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Nostr {
|
||||
#[serde(skip_serializing)]
|
||||
client: Client,
|
||||
contact_list: Mutex<Vec<Contact>>,
|
||||
settings: Mutex<Settings>,
|
||||
@@ -47,28 +42,45 @@ pub struct Settings {
|
||||
display_zap_button: bool,
|
||||
display_repost_button: bool,
|
||||
display_media: bool,
|
||||
vibrancy: bool,
|
||||
transparent: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
proxy: None,
|
||||
image_resize_service: Some("https://wsrv.nl/".into()),
|
||||
image_resize_service: Some("https://wsrv.nl".to_string()),
|
||||
use_relay_hint: true,
|
||||
content_warning: true,
|
||||
display_avatar: true,
|
||||
display_zap_button: true,
|
||||
display_repost_button: true,
|
||||
display_media: true,
|
||||
vibrancy: true,
|
||||
transparent: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const FETCH_LIMIT: usize = 20;
|
||||
#[derive(Serialize, Deserialize, Type)]
|
||||
enum SubKind {
|
||||
Subscribe,
|
||||
Unsubscribe,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, TauriEvent)]
|
||||
struct Subscription {
|
||||
label: String,
|
||||
kind: SubKind,
|
||||
event_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Clone, TauriEvent)]
|
||||
struct NewSettings(Settings);
|
||||
|
||||
pub const FETCH_LIMIT: usize = 44;
|
||||
pub const NEWSFEED_NEG_LIMIT: usize = 256;
|
||||
pub const NOTIFICATION_NEG_LIMIT: usize = 64;
|
||||
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
|
||||
|
||||
fn main() {
|
||||
let builder = Builder::<tauri::Wry>::new()
|
||||
@@ -83,54 +95,57 @@ fn main() {
|
||||
create_account,
|
||||
import_account,
|
||||
connect_account,
|
||||
get_private_key,
|
||||
delete_account,
|
||||
reset_password,
|
||||
login,
|
||||
get_profile,
|
||||
set_profile,
|
||||
get_contact_list,
|
||||
set_contact_list,
|
||||
create_profile,
|
||||
is_contact_list_empty,
|
||||
check_contact,
|
||||
toggle_contact,
|
||||
get_nstore,
|
||||
set_nstore,
|
||||
get_lume_store,
|
||||
set_lume_store,
|
||||
set_wallet,
|
||||
load_wallet,
|
||||
remove_wallet,
|
||||
zap_profile,
|
||||
zap_event,
|
||||
friend_to_friend,
|
||||
copy_friend,
|
||||
get_notifications,
|
||||
get_settings,
|
||||
set_new_settings,
|
||||
set_settings,
|
||||
verify_nip05,
|
||||
get_event_meta,
|
||||
get_event,
|
||||
get_event_from,
|
||||
get_replies,
|
||||
listen_event_reply,
|
||||
subscribe_to,
|
||||
get_events_by,
|
||||
get_local_events,
|
||||
listen_local_event,
|
||||
get_events_from_contacts,
|
||||
get_group_events,
|
||||
get_global_events,
|
||||
get_hashtag_events,
|
||||
search,
|
||||
publish,
|
||||
reply,
|
||||
repost,
|
||||
event_to_bech32,
|
||||
user_to_bech32,
|
||||
unlisten,
|
||||
create_column,
|
||||
close_column,
|
||||
reposition_column,
|
||||
resize_column,
|
||||
reload_column,
|
||||
open_window,
|
||||
open_main_window,
|
||||
force_quit
|
||||
]);
|
||||
reopen_lume,
|
||||
quit
|
||||
])
|
||||
.events(collect_events![Subscription, NewSettings]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
builder
|
||||
.export(Typescript::default(), "../src/commands.gen.ts")
|
||||
.expect("Failed to export typescript bindings");
|
||||
@@ -148,10 +163,9 @@ fn main() {
|
||||
.setup(move |app| {
|
||||
builder.mount_events(app);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
app.handle().plugin(tauri_nspanel::init()).unwrap();
|
||||
|
||||
let handle = app.handle();
|
||||
let handle_clone = handle.clone();
|
||||
let handle_clone_child = handle_clone.clone();
|
||||
let main_window = app.get_webview_window("main").unwrap();
|
||||
|
||||
// Set custom decoration for Windows
|
||||
@@ -164,7 +178,7 @@ fn main() {
|
||||
|
||||
// Set a custom inset to the traffic lights
|
||||
#[cfg(target_os = "macos")]
|
||||
main_window.set_traffic_lights_inset(8.0, 16.0).unwrap();
|
||||
main_window.set_traffic_lights_inset(7.0, 10.0).unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let win = main_window.clone();
|
||||
@@ -172,25 +186,29 @@ fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
main_window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::ThemeChanged(_) = event {
|
||||
win.set_traffic_lights_inset(8.0, 16.0).unwrap();
|
||||
win.set_traffic_lights_inset(7.0, 10.0).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// Create data folder if not exist
|
||||
let home_dir = app.path().home_dir().unwrap();
|
||||
let _ = fs::create_dir_all(home_dir.join("Lume/"));
|
||||
|
||||
let client = tauri::async_runtime::block_on(async move {
|
||||
// Create data folder if not exist
|
||||
let dir = handle
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.expect("App config directory not found.");
|
||||
let _ = fs::create_dir_all(dir.clone());
|
||||
|
||||
// Setup database
|
||||
let database = SQLiteDatabase::open(home_dir.join("Lume/lume.db"))
|
||||
let database = SQLiteDatabase::open(dir.join("nostr.db"))
|
||||
.await
|
||||
.expect("Error.");
|
||||
.expect("Database error.");
|
||||
|
||||
// Config
|
||||
let opts = Options::new()
|
||||
.max_avg_latency(Duration::from_millis(500))
|
||||
.automatic_authentication(true)
|
||||
.connection_timeout(Some(Duration::from_secs(5)))
|
||||
.timeout(Duration::from_secs(30));
|
||||
.timeout(Duration::from_secs(20));
|
||||
|
||||
// Setup nostr client
|
||||
let client = ClientBuilder::default()
|
||||
@@ -211,10 +229,6 @@ fn main() {
|
||||
if let Some((relay, option)) = line.split_once(',') {
|
||||
match RelayMetadata::from_str(option) {
|
||||
Ok(meta) => {
|
||||
println!(
|
||||
"connecting to bootstrap relay...: {} - {}",
|
||||
relay, meta
|
||||
);
|
||||
let opts = if meta == RelayMetadata::Read {
|
||||
RelayOptions::new().read(true).write(false)
|
||||
} else {
|
||||
@@ -223,7 +237,6 @@ fn main() {
|
||||
let _ = client.add_relay_with_opts(relay, opts).await;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("connecting to bootstrap relay...: {}", relay);
|
||||
let _ = client.add_relay(relay).await;
|
||||
}
|
||||
}
|
||||
@@ -244,11 +257,116 @@ fn main() {
|
||||
settings: Mutex::new(Settings::default()),
|
||||
});
|
||||
|
||||
Subscription::listen_any(app, move |event| {
|
||||
let handle = handle_clone_child.to_owned();
|
||||
let payload = event.payload;
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
|
||||
match payload.kind {
|
||||
SubKind::Subscribe => {
|
||||
let subscription_id = SubscriptionId::new(payload.label);
|
||||
|
||||
let filter = if let Some(id) = payload.event_id {
|
||||
let event_id = EventId::from_str(&id).unwrap();
|
||||
|
||||
Filter::new().event(event_id).since(Timestamp::now())
|
||||
} else {
|
||||
let contact_list = state.contact_list.lock().await;
|
||||
let authors: Vec<PublicKey> =
|
||||
contact_list.iter().map(|f| f.public_key).collect();
|
||||
|
||||
Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.authors(authors)
|
||||
.since(Timestamp::now())
|
||||
};
|
||||
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id(subscription_id, vec![filter], None)
|
||||
.await
|
||||
{
|
||||
println!("Subscription error: {}", e)
|
||||
}
|
||||
}
|
||||
SubKind::Unsubscribe => {
|
||||
let subscription_id = SubscriptionId::new(payload.label);
|
||||
client.unsubscribe(subscription_id).await
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle_clone.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
|
||||
let allow_notification = match handle_clone.notification().request_permission() {
|
||||
Ok(_) => {
|
||||
if let Ok(perm) = handle_clone.notification().permission_state() {
|
||||
PermissionState::Granted == perm
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
};
|
||||
|
||||
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
||||
|
||||
client
|
||||
.handle_notifications(|notification| async {
|
||||
if let RelayPoolNotification::Message { message, .. } = notification {
|
||||
if let RelayMessage::Event {
|
||||
subscription_id,
|
||||
event,
|
||||
} = message
|
||||
{
|
||||
// Handle events from notification subscription
|
||||
if subscription_id == notification_id {
|
||||
// Send native notification
|
||||
if allow_notification {
|
||||
let author = client
|
||||
.metadata(event.pubkey)
|
||||
.await
|
||||
.unwrap_or_else(|_| Metadata::new());
|
||||
|
||||
send_notification(&event, author, &handle_clone);
|
||||
}
|
||||
}
|
||||
|
||||
let label = subscription_id.to_string();
|
||||
let raw = event.as_json();
|
||||
let parsed = if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
handle_clone
|
||||
.emit_to(
|
||||
EventTarget::labeled(label),
|
||||
"event",
|
||||
RichEvent { raw, parsed },
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
println!("new message: {}", message.as_json())
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
})
|
||||
.await
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.plugin(tauri_plugin_prevent_default::init())
|
||||
.plugin(prevent_default())
|
||||
.plugin(tauri_plugin_theme::init(ctx.config_mut()))
|
||||
.plugin(tauri_plugin_decorum::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
@@ -262,3 +380,56 @@ fn main() {
|
||||
.run(ctx)
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
|
||||
tauri_plugin_prevent_default::Builder::new()
|
||||
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||||
tauri_plugin_prevent_default::Builder::new().build()
|
||||
}
|
||||
|
||||
fn send_notification(event: &Event, author: Metadata, handle: &tauri::AppHandle) {
|
||||
match event.kind() {
|
||||
Kind::TextNote => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Mentioned you in a thread.")
|
||||
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
|
||||
.show()
|
||||
{
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
Kind::Repost => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Reposted your note.")
|
||||
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
|
||||
.show()
|
||||
{
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
Kind::ZapReceipt => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Zapped you.")
|
||||
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
|
||||
.show()
|
||||
{
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user