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:
雨宮蓮
2024-08-27 19:37:30 +07:00
committed by GitHub
parent 26ae473521
commit 61ad96ca63
318 changed files with 5564 additions and 8458 deletions

View File

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

View File

@@ -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()),
}
}

View File

@@ -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()),
}
}

View File

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

View File

@@ -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);
}

View File

@@ -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)
}