feat: Multi Accounts (#237)

* wip: new sync

* wip: restructure routes

* update

* feat: improve sync

* feat: repost with multi-account

* feat: improve sync

* feat: publish with multi account

* fix: settings screen

* feat: add zap for multi accounts
This commit is contained in:
雨宮蓮
2024-10-22 16:00:06 +07:00
committed by GitHub
parent ba9c81a10a
commit cc7de41bfd
89 changed files with 2695 additions and 2911 deletions

View File

@@ -1,17 +1,11 @@
use keyring::Entry;
use keyring_search::{Limit, List, Search};
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::{
collections::HashSet,
fs::{self, File},
str::FromStr,
time::Duration,
};
use tauri::{Emitter, Manager, State};
use std::{str::FromStr, time::Duration};
use tauri::{Emitter, State};
use crate::{Nostr, NOTIFICATION_SUB_ID};
use crate::{common::get_all_accounts, Nostr};
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
struct Account {
@@ -22,67 +16,31 @@ struct Account {
#[tauri::command]
#[specta::specta]
pub fn get_accounts() -> Vec<String> {
let search = Search::new().expect("Unexpected.");
let results = search.by_service("Lume Secret Storage");
let list = List::list_credentials(&results, Limit::All);
let accounts: HashSet<String> = list
.split_whitespace()
.filter(|v| v.starts_with("npub1"))
.map(String::from)
.collect();
accounts.into_iter().collect()
get_all_accounts()
}
#[tauri::command]
#[specta::specta]
pub async fn create_account(
name: String,
about: String,
picture: String,
pub async fn watch_account(key: String, state: State<'_, Nostr>) -> Result<String, String> {
let public_key = PublicKey::from_str(&key).map_err(|e| e.to_string())?;
let bech32 = public_key.to_bech32().map_err(|e| e.to_string())?;
let keyring = Entry::new("Lume Secret Storage", &bech32).map_err(|e| e.to_string())?;
keyring.set_password("").map_err(|e| e.to_string())?;
// Update state
state.accounts.lock().unwrap().push(bech32.clone());
Ok(bech32)
}
#[tauri::command]
#[specta::specta]
pub async fn import_account(
key: String,
password: String,
state: State<'_, Nostr>,
) -> Result<String, String> {
let client = &state.client;
let keys = Keys::generate();
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
let secret_key = keys.secret_key();
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())?;
// Save account
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);
let signer = NostrSigner::Keys(keys);
// Update signer
client.set_signer(Some(signer)).await;
let mut metadata = Metadata::new()
.display_name(name.clone())
.name(name.to_lowercase())
.about(about);
if let Ok(url) = Url::parse(&picture) {
metadata = metadata.picture(url)
}
match client.set_metadata(&metadata).await {
Ok(_) => Ok(npub),
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn import_account(key: String, password: String) -> Result<String, String> {
let (npub, enc_bech32) = match key.starts_with("ncryptsec") {
true => {
let enc = EncryptedSecretKey::from_bech32(key).map_err(|err| err.to_string())?;
@@ -117,6 +75,9 @@ pub async fn import_account(key: String, password: String) -> Result<String, Str
let pwd = serde_json::to_string(&account).map_err(|e| e.to_string())?;
keyring.set_password(&pwd).map_err(|e| e.to_string())?;
// Update state
state.accounts.lock().unwrap().push(npub.clone());
Ok(npub)
}
@@ -157,6 +118,9 @@ pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result<Str
// Update signer
let _ = client.set_signer(Some(signer.into())).await;
// Update state
state.accounts.lock().unwrap().push(remote_npub.clone());
Ok(remote_npub)
}
Err(err) => Err(err.to_string()),
@@ -208,34 +172,32 @@ pub fn delete_account(id: String) -> Result<(), String> {
#[tauri::command]
#[specta::specta]
pub fn is_account_sync(id: String, handle: tauri::AppHandle) -> bool {
let config_dir = handle
.path()
.app_config_dir()
.expect("Error: app config directory not found.");
pub async fn has_signer(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
fs::metadata(config_dir.join(id)).is_ok()
match client.signer().await {
Ok(signer) => {
// Emit reload in front-end
// handle.emit("signer", ()).unwrap();
let signer_key = signer.public_key().await.map_err(|e| e.to_string())?;
let is_match = signer_key == public_key;
Ok(is_match)
}
Err(_) => Ok(false),
}
}
#[tauri::command]
#[specta::specta]
pub fn create_sync_file(id: String, handle: tauri::AppHandle) -> bool {
let config_dir = handle
.path()
.app_config_dir()
.expect("Error: app config directory not found.");
File::create(config_dir.join(id)).is_ok()
}
#[tauri::command]
#[specta::specta]
pub async fn login(
pub async fn set_signer(
account: String,
password: String,
state: State<'_, Nostr>,
handle: tauri::AppHandle,
) -> Result<String, String> {
) -> Result<(), String> {
let client = &state.client;
let keyring = Entry::new("Lume Secret Storage", &account).map_err(|e| e.to_string())?;
@@ -247,7 +209,7 @@ pub async fn login(
Err(e) => return Err(e.to_string()),
};
let public_key = match account.nostr_connect {
match account.nostr_connect {
None => {
let ncryptsec =
EncryptedSecretKey::from_bech32(account.password).map_err(|e| e.to_string())?;
@@ -255,196 +217,30 @@ pub async fn login(
.to_secret_key(password)
.map_err(|_| "Wrong password.")?;
let keys = Keys::new(secret_key);
let public_key = keys.public_key().to_bech32().unwrap();
let signer = NostrSigner::Keys(keys);
// Update signer
client.set_signer(Some(signer)).await;
// Emit to front-end
handle.emit("signer-updated", ()).unwrap();
public_key
Ok(())
}
Some(bunker) => {
let uri = NostrConnectURI::parse(bunker).map_err(|e| e.to_string())?;
let public_key = uri.signer_public_key().unwrap().to_bech32().unwrap();
let app_keys = Keys::from_str(&account.password).map_err(|e| e.to_string())?;
match Nip46Signer::new(uri, app_keys, Duration::from_secs(120), None) {
Ok(signer) => {
// Update signer
client.set_signer(Some(signer.into())).await;
public_key
// Emit to front-end
handle.emit("signer-updated", ()).unwrap();
Ok(())
}
Err(e) => return Err(e.to_string()),
Err(e) => Err(e.to_string()),
}
}
};
// NIP-65: Connect to user's relay list
// init_nip65(client, &public_key).await;
// NIP-03: Get user's contact list
let contact_list = {
if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await {
state.contact_list.lock().unwrap().clone_from(&contacts);
contacts
} else {
Vec::new()
}
};
let public_key_clone = public_key.clone();
// Run seperate thread for sync
tauri::async_runtime::spawn(async move {
let state = handle.state::<Nostr>();
let client = &state.client;
let author = PublicKey::from_str(&public_key).unwrap();
// Subscribe for new notification
if let Ok(e) = client
.subscribe_with_id(
SubscriptionId::new(NOTIFICATION_SUB_ID),
vec![Filter::new().pubkey(author).since(Timestamp::now())],
None,
)
.await
{
println!("Subscribed: {}", e.success.len())
}
// Get events from contact list
if !contact_list.is_empty() {
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
// Syncing all metadata events from contact list
if let Ok(report) = client
.reconcile(
Filter::new()
.authors(authors.clone())
.kinds(vec![Kind::Metadata, Kind::ContactList])
.limit(authors.len() * 10),
NegentropyOptions::default(),
)
.await
{
println!("Received: {}", report.received.len());
}
// Syncing all events from contact list
if let Ok(report) = client
.reconcile(
Filter::new()
.authors(authors.clone())
.kinds(vec![Kind::TextNote, Kind::Repost, Kind::EventDeletion])
.limit(authors.len() * 40),
NegentropyOptions::default(),
)
.await
{
println!("Received: {}", report.received.len());
}
// Create the trusted public key list from contact list
// TODO: create a cached file
if let Ok(events) = client
.database()
.query(vec![Filter::new().kind(Kind::ContactList)])
.await
{
let keys: Vec<&str> = events
.iter()
.flat_map(|event| {
event
.tags
.iter()
.filter(|t| t.kind() == TagKind::p())
.filter_map(|t| t.content())
.collect::<Vec<&str>>()
})
.collect();
let trusted_list: HashSet<PublicKey> = keys
.into_iter()
.filter_map(|item| {
if let Ok(pk) = PublicKey::from_str(item) {
Some(pk)
} else {
None
}
})
.collect();
// Update app's state
state.trusted_list.lock().unwrap().clone_from(&trusted_list);
let trusted_users: Vec<PublicKey> = trusted_list.into_iter().collect();
println!("Total trusted users: {}", trusted_users.len());
if let Ok(report) = client
.reconcile(
Filter::new()
.authors(trusted_users)
.kinds(vec![
Kind::Metadata,
Kind::TextNote,
Kind::Repost,
Kind::EventDeletion,
])
.limit(5000),
NegentropyOptions::default(),
)
.await
{
println!("Received: {}", report.received.len())
}
};
};
// Syncing all user's events
if let Ok(report) = client
.reconcile(
Filter::new().author(author).kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::FollowSet,
Kind::InterestSet,
Kind::Interests,
Kind::EventDeletion,
Kind::MuteList,
Kind::BookmarkSet,
Kind::BlockedRelays,
Kind::EmojiSet,
Kind::RelaySet,
Kind::RelayList,
Kind::ApplicationSpecificData,
]),
NegentropyOptions::default(),
)
.await
{
println!("Received: {}", report.received.len())
}
// Syncing all tagged events for current user
if let Ok(report) = client
.reconcile(
Filter::new().pubkey(author).kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
]),
NegentropyOptions::default(),
)
.await
{
println!("Received: {}", report.received.len())
};
handle
.emit("neg_synchronized", ())
.expect("Something wrong!");
});
Ok(public_key_clone)
}
}

View File

@@ -410,16 +410,58 @@ pub async fn repost(raw: String, state: State<'_, Nostr>) -> Result<String, Stri
#[tauri::command]
#[specta::specta]
pub async fn delete(id: String, state: State<'_, Nostr>) -> Result<String, String> {
pub async fn is_reposted(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let accounts = state.accounts.lock().unwrap().clone();
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
match client.delete_event(event_id).await {
Ok(event_id) => Ok(event_id.to_string()),
let authors: Vec<PublicKey> = accounts
.iter()
.map(|acc| PublicKey::from_str(acc).unwrap())
.collect();
let filter = Filter::new()
.event(event_id)
.kind(Kind::Repost)
.authors(authors);
match client.database().query(vec![filter]).await {
Ok(events) => Ok(!events.is_empty()),
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> {
let client = &state.client;
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
match client.delete_event(event_id).await {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let signer = client.signer().await.map_err(|err| err.to_string())?;
let public_key = signer.public_key().await.map_err(|err| err.to_string())?;
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
let filter = Filter::new()
.author(public_key)
.event(event_id)
.kind(Kind::EventDeletion);
match client.database().query(vec![filter]).await {
Ok(events) => Ok(!events.is_empty()),
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn event_to_bech32(id: String, state: State<'_, Nostr>) -> Result<String, String> {
@@ -498,34 +540,3 @@ pub async fn search(query: String, state: State<'_, Nostr>) -> Result<Vec<RichEv
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let signer = client.signer().await.map_err(|err| err.to_string())?;
let public_key = signer.public_key().await.map_err(|err| err.to_string())?;
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
let filter = Filter::new()
.author(public_key)
.event(event_id)
.kind(Kind::EventDeletion);
match client.database().query(vec![filter]).await {
Ok(events) => Ok(!events.is_empty()),
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> {
let client = &state.client;
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
let builder = EventBuilder::delete(vec![event_id]);
match client.send_event_builder(builder).await {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}

View File

@@ -4,11 +4,10 @@ use serde::{Deserialize, Serialize};
use specta::Type;
use std::{str::FromStr, time::Duration};
use tauri::{Emitter, Manager, State};
use tauri_specta::Event;
use crate::{
common::{get_latest_event, process_event},
NewSettings, Nostr, RichEvent, Settings,
common::{get_all_accounts, get_latest_event, get_tags_content, process_event},
Nostr, RichEvent, Settings,
};
#[derive(Clone, Serialize, Deserialize, Type)]
@@ -104,14 +103,36 @@ pub async fn set_contact_list(
#[tauri::command]
#[specta::specta]
pub fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let contact_list = state.contact_list.lock().unwrap().clone();
let vec: Vec<String> = contact_list
.into_iter()
.map(|f| f.public_key.to_hex())
.collect();
pub async fn get_contact_list(id: String, state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client;
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
Ok(vec)
let filter = Filter::new()
.author(public_key)
.kind(Kind::ContactList)
.limit(1);
let mut contact_list: Vec<String> = Vec::new();
match client.database().query(vec![filter]).await {
Ok(events) => {
if let Some(event) = events.into_iter().next() {
for tag in event.tags.into_iter() {
if let Some(TagStandard::PublicKey {
public_key,
uppercase: false,
..
}) = tag.to_standardized()
{
contact_list.push(public_key.to_hex())
}
}
}
Ok(contact_list)
}
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
@@ -149,13 +170,27 @@ pub async fn set_profile(profile: Profile, state: State<'_, Nostr>) -> Result<St
#[tauri::command]
#[specta::specta]
pub fn check_contact(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
let contact_list = &state.contact_list.lock().unwrap();
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
pub async fn is_contact(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
match contact_list.iter().position(|x| x.public_key == public_key) {
Some(_) => Ok(true),
None => Ok(false),
let filter = Filter::new()
.author(public_key)
.kind(Kind::ContactList)
.limit(1);
match client.database().query(vec![filter]).await {
Ok(events) => {
if let Some(event) = events.into_iter().next() {
let hex = public_key.to_hex();
let pubkeys = get_tags_content(&event, TagKind::p());
Ok(pubkeys.iter().any(|i| i == &hex))
} else {
Ok(false)
}
}
Err(e) => Err(e.to_string()),
}
}
@@ -267,9 +302,18 @@ pub async fn get_group(id: String, state: State<'_, Nostr>) -> Result<String, St
#[specta::specta]
pub async fn get_all_groups(state: State<'_, Nostr>) -> Result<Vec<RichEvent>, 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())?;
let filter = Filter::new().kind(Kind::FollowSet).author(public_key);
let accounts = get_all_accounts();
let authors: Vec<PublicKey> = accounts
.iter()
.filter_map(|acc| {
if let Ok(pk) = PublicKey::from_str(acc) {
Some(pk)
} else {
None
}
})
.collect();
let filter = Filter::new().kind(Kind::FollowSet).authors(authors);
match client.database().query(vec![filter]).await {
Ok(events) => Ok(process_event(client, events).await),
@@ -347,11 +391,20 @@ pub async fn get_interest(id: String, state: State<'_, Nostr>) -> Result<String,
#[specta::specta]
pub async fn get_all_interests(state: State<'_, Nostr>) -> Result<Vec<RichEvent>, 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())?;
let accounts = get_all_accounts();
let authors: Vec<PublicKey> = accounts
.iter()
.filter_map(|acc| {
if let Ok(pk) = PublicKey::from_str(acc) {
Some(pk)
} else {
None
}
})
.collect();
let filter = Filter::new()
.kinds(vec![Kind::InterestSet, Kind::Interests])
.author(public_key);
.authors(authors);
match client.database().query(vec![filter]).await {
Ok(events) => Ok(process_event(client, events).await),
@@ -361,7 +414,7 @@ pub async fn get_all_interests(state: State<'_, Nostr>) -> Result<Vec<RichEvent>
#[tauri::command]
#[specta::specta]
pub async fn get_mention_list(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
pub async fn get_all_profiles(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
let client = &state.client;
let filter = Filter::new().kind(Kind::Metadata);
@@ -396,7 +449,9 @@ pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, Stri
if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) {
let nwc = NWC::new(nwc_uri);
let keyring = Entry::new("Lume Secret", "Bitcoin Connect").map_err(|e| e.to_string())?;
let keyring =
Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?;
keyring.set_password(uri).map_err(|e| e.to_string())?;
client.set_zapper(nwc).await;
@@ -408,29 +463,25 @@ pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, Stri
#[tauri::command]
#[specta::specta]
pub async fn load_wallet(state: State<'_, Nostr>) -> Result<String, String> {
pub async fn load_wallet(state: State<'_, Nostr>) -> Result<(), String> {
let client = &state.client;
let keyring =
Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?;
match keyring.get_password() {
Ok(val) => {
let uri = NostrWalletConnectURI::from_str(&val).unwrap();
let nwc = NWC::new(uri);
if client.zapper().await.is_err() {
let keyring =
Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?;
// Get current balance
let balance = nwc.get_balance().await;
match keyring.get_password() {
Ok(val) => {
let uri = NostrWalletConnectURI::from_str(&val).unwrap();
let nwc = NWC::new(uri);
// Update zapper
client.set_zapper(nwc).await;
match balance {
Ok(val) => Ok(val.to_string()),
Err(_) => Err("Get balance failed.".into()),
client.set_zapper(nwc).await;
}
Err(_) => return Err("Wallet not found.".into()),
}
Err(_) => Err("NWC not found.".into()),
}
Ok(())
}
#[tauri::command]
@@ -452,52 +503,40 @@ pub async fn remove_wallet(state: State<'_, Nostr>) -> Result<(), String> {
#[tauri::command]
#[specta::specta]
pub async fn zap_profile(
id: &str,
amount: &str,
message: &str,
id: String,
amount: String,
message: Option<String>,
state: State<'_, Nostr>,
) -> Result<bool, String> {
) -> Result<(), String> {
let client = &state.client;
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())?;
let details = message.map(|m| ZapDetails::new(ZapType::Public).message(m));
if client.zap(public_key, num, Some(details)).await.is_ok() {
Ok(true)
} else {
Err("Zap profile failed".into())
match client.zap(public_key, num, details).await {
Ok(()) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn zap_event(
id: &str,
amount: &str,
message: &str,
id: String,
amount: String,
message: Option<String>,
state: State<'_, Nostr>,
) -> Result<bool, String> {
) -> Result<(), 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 invalid.".into()),
},
Err(_) => match EventId::from_hex(id) {
Ok(val) => val,
Err(_) => return Err("Event ID is invalid.".into()),
},
};
let details = ZapDetails::new(ZapType::Private).message(message);
let event_id = EventId::from_str(&id).map_err(|e| e.to_string())?;
let num = amount.parse::<u64>().map_err(|e| e.to_string())?;
let details = message.map(|m| ZapDetails::new(ZapType::Public).message(m));
if client.zap(event_id, num, Some(details)).await.is_ok() {
Ok(true)
} else {
Err("Zap event failed".into())
match client.zap(event_id, num, details).await {
Ok(()) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
@@ -544,27 +583,22 @@ pub async fn copy_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, St
#[tauri::command]
#[specta::specta]
pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
pub async fn get_notifications(id: String, state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client;
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
match client.signer().await {
Ok(signer) => {
let public_key = signer.public_key().await.unwrap();
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.limit(200);
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.limit(200);
match client.database().query(vec![filter]).await {
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
Err(err) => Err(err.to_string()),
}
}
match client.database().query(vec![filter]).await {
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
Err(err) => Err(err.to_string()),
}
}
@@ -577,29 +611,11 @@ pub fn get_user_settings(state: State<'_, Nostr>) -> Result<Settings, String> {
#[tauri::command]
#[specta::specta]
pub async fn set_user_settings(
settings: String,
state: State<'_, Nostr>,
handle: tauri::AppHandle,
) -> Result<(), String> {
let client = &state.client;
let tags = vec![Tag::identifier("lume_user_setting")];
let builder = EventBuilder::new(Kind::ApplicationSpecificData, &settings, tags);
pub async fn set_user_settings(settings: String, state: State<'_, Nostr>) -> Result<(), String> {
let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?;
state.settings.lock().unwrap().clone_from(&parsed);
match client.send_event_builder(builder).await {
Ok(_) => {
let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?;
// Update state
state.settings.lock().unwrap().clone_from(&parsed);
// Emit new changes to frontend
NewSettings(parsed).emit(&handle).unwrap();
Ok(())
}
Err(err) => Err(err.to_string()),
}
Ok(())
}
#[tauri::command]
@@ -613,12 +629,3 @@ pub async fn verify_nip05(id: String, nip05: &str) -> Result<bool, String> {
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub fn is_trusted_user(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
let trusted_list = &state.trusted_list.lock().unwrap();
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
Ok(trusted_list.contains(&public_key))
}

View File

@@ -2,4 +2,5 @@ pub mod account;
pub mod event;
pub mod metadata;
pub mod relay;
pub mod sync;
pub mod window;

View File

@@ -5,6 +5,7 @@ use specta::Type;
use std::{
fs::OpenOptions,
io::{self, BufRead, Write},
str::FromStr,
};
use tauri::{path::BaseDirectory, Manager, State};
@@ -18,8 +19,9 @@ pub struct Relays {
#[tauri::command]
#[specta::specta]
pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, String> {
pub async fn get_relays(id: String, state: State<'_, Nostr>) -> Result<Relays, String> {
let client = &state.client;
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
let connected_relays = client
.relays()
@@ -28,9 +30,6 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, String> {
.map(|url| url.to_string())
.collect::<Vec<_>>();
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 filter = Filter::new()
.author(public_key)
.kind(Kind::RelayList)
@@ -98,13 +97,14 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, String> {
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let status = client.add_relay(relay).await.map_err(|e| e.to_string())?;
if status {
println!("Connecting to relay: {}", relay);
client
.connect_relay(relay)
.await
.map_err(|e| e.to_string())?;
}
Ok(status)
}
@@ -112,14 +112,12 @@ pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool,
#[specta::specta]
pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
client
.remove_relay(relay)
.await
.map_err(|e| e.to_string())?;
client
.disconnect_relay(relay)
.force_remove_relay(relay)
.await
.map_err(|e| e.to_string())?;
Ok(true)
}

View File

@@ -0,0 +1,204 @@
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::str::FromStr;
use tauri::{AppHandle, Manager};
use tauri_specta::Event as TauriEvent;
use crate::{common::get_tags_content, Nostr};
#[derive(Clone, Serialize, Type, TauriEvent)]
pub struct NegentropyEvent {
kind: NegentropyKind,
total_event: i32,
}
#[derive(Clone, Serialize, Deserialize, Type)]
pub enum NegentropyKind {
Profile,
Metadata,
Events,
EventIds,
Global,
Notification,
Others,
}
pub fn run_fast_sync(accounts: Vec<String>, app_handle: AppHandle) {
if accounts.is_empty() {
return;
};
let public_keys: Vec<PublicKey> = accounts
.iter()
.filter_map(|acc| {
if let Ok(pk) = PublicKey::from_str(acc) {
Some(pk)
} else {
None
}
})
.collect();
tauri::async_runtime::spawn(async move {
let state = app_handle.state::<Nostr>();
let client = &state.client;
let bootstrap_relays = state.bootstrap_relays.lock().unwrap().clone();
// NEG: Sync profile
//
let profile = Filter::new()
.authors(public_keys.clone())
.kind(Kind::Metadata)
.limit(4);
if let Ok(report) = client
.reconcile_with(&bootstrap_relays, profile, NegentropyOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Profile,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
// NEG: Sync contact list
//
let contact_list = Filter::new()
.authors(public_keys.clone())
.kind(Kind::ContactList)
.limit(4);
if let Ok(report) = client
.reconcile_with(
&bootstrap_relays,
contact_list.clone(),
NegentropyOptions::default(),
)
.await
{
NegentropyEvent {
kind: NegentropyKind::Metadata,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
// NEG: Sync events from contact list
//
if let Ok(events) = client.database().query(vec![contact_list]).await {
let pubkeys: Vec<PublicKey> = events
.iter()
.flat_map(|ev| {
let tags = get_tags_content(ev, TagKind::p());
tags.into_iter().filter_map(|p| {
if let Ok(pk) = PublicKey::from_hex(p) {
Some(pk)
} else {
None
}
})
})
.collect();
for chunk in pubkeys.chunks(500) {
if chunk.is_empty() {
break;
}
let authors = chunk.to_owned();
// NEG: Sync event
//
let events = Filter::new()
.authors(authors.clone())
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(1000);
if let Ok(report) = client
.reconcile_with(&bootstrap_relays, events, NegentropyOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Events,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
// NEG: Sync metadata
//
let metadata = Filter::new()
.authors(authors)
.kind(Kind::Metadata)
.limit(1000);
if let Ok(report) = client
.reconcile_with(&bootstrap_relays, metadata, NegentropyOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Metadata,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
}
}
// NEG: Sync other metadata
//
let others = Filter::new().authors(public_keys.clone()).kinds(vec![
Kind::Interests,
Kind::InterestSet,
Kind::FollowSet,
Kind::EventDeletion,
Kind::Custom(30315),
]);
if let Ok(report) = client
.reconcile_with(&bootstrap_relays, others, NegentropyOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Others,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
// NEG: Sync notification
//
let notification = Filter::new()
.pubkeys(public_keys)
.kinds(vec![
Kind::Reaction,
Kind::TextNote,
Kind::Repost,
Kind::ZapReceipt,
])
.limit(10000);
if let Ok(report) = client
.reconcile_with(
&bootstrap_relays,
notification,
NegentropyOptions::default(),
)
.await
{
NegentropyEvent {
kind: NegentropyKind::Notification,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
});
}

View File

@@ -22,6 +22,7 @@ pub struct Window {
maximizable: bool,
minimizable: bool,
hidden_title: bool,
closable: bool,
}
#[derive(Serialize, Deserialize, Type)]
@@ -109,7 +110,7 @@ pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<bool
#[tauri::command]
#[specta::specta]
#[cfg(target_os = "macos")]
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<String, String> {
if let Some(current_window) = app_handle.get_window(&window.label) {
if current_window.is_visible().unwrap_or_default() {
let _ = current_window.set_focus();
@@ -117,6 +118,8 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
let _ = current_window.show();
let _ = current_window.set_focus();
};
Ok(current_window.label().to_string())
} else {
let new_window = WebviewWindowBuilder::new(
&app_handle,
@@ -131,6 +134,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
.minimizable(window.minimizable)
.maximizable(window.maximizable)
.transparent(true)
.closable(window.closable)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::UnderWindowBackground],
@@ -142,24 +146,26 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
// Restore native border
new_window.add_border(None);
}
Ok(())
Ok(new_window.label().to_string())
}
}
#[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();
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<String, String> {
if let Some(current_window) = app_handle.get_window(&window.label) {
if current_window.is_visible().unwrap_or_default() {
let _ = current_window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
let _ = current_window.show();
let _ = current_window.set_focus();
};
Ok(current_window.label().to_string())
} else {
let window = WebviewWindowBuilder::new(
let new_window = WebviewWindowBuilder::new(
&app_handle,
&window.label,
WebviewUrl::App(PathBuf::from(window.url)),
@@ -171,6 +177,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
.maximizable(window.maximizable)
.transparent(true)
.decorations(false)
.closable(window.closable)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::Mica],
@@ -181,7 +188,9 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
.unwrap();
// Set decoration
window.create_overlay_titlebar().unwrap();
new_window.create_overlay_titlebar().unwrap();
Ok(new_window.label().to_string())
}
Ok(())

View File

@@ -1,10 +1,11 @@
use futures::future::join_all;
use keyring_search::{Limit, List, Search};
use linkify::LinkFinder;
use nostr_sdk::prelude::*;
use reqwest::Client as ReqClient;
use serde::Serialize;
use specta::Type;
use std::{collections::HashSet, str::FromStr, time::Duration};
use std::{collections::HashSet, str::FromStr};
use crate::RichEvent;
@@ -18,6 +19,8 @@ pub struct Meta {
}
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
// const VIDEOS: [&str; 6] = ["mp4", "avi", "mov", "mkv", "wmv", "webm"];
const NOSTR_EVENTS: [&str; 10] = [
"@nevent1",
"@note1",
@@ -30,6 +33,7 @@ const NOSTR_EVENTS: [&str; 10] = [
"Nostr:note1",
"Nostr:nevent1",
];
const NOSTR_MENTIONS: [&str; 10] = [
"@npub1",
"nostr:npub1",
@@ -47,6 +51,15 @@ pub fn get_latest_event(events: &Events) -> Option<&Event> {
events.iter().next()
}
pub fn get_tags_content(event: &Event, kind: TagKind) -> Vec<String> {
event
.tags
.iter()
.filter(|t| t.kind() == kind)
.filter_map(|t| t.content().map(|content| content.to_string()))
.collect()
}
pub fn create_tags(content: &str) -> Vec<Tag> {
let mut tags: Vec<Tag> = vec![];
let mut tag_set: HashSet<String> = HashSet::new();
@@ -65,7 +78,7 @@ pub fn create_tags(content: &str) -> Vec<Tag> {
let hashtags = words
.iter()
.filter(|&&word| word.starts_with('#'))
.map(|&s| s.to_string())
.map(|&s| s.to_string().replace("#", "").to_lowercase())
.collect::<Vec<_>>();
for mention in mentions {
@@ -128,6 +141,19 @@ pub fn create_tags(content: &str) -> Vec<Tag> {
tags
}
pub fn get_all_accounts() -> Vec<String> {
let search = Search::new().expect("Unexpected.");
let results = search.by_service("Lume Secret Storage");
let list = List::list_credentials(&results, Limit::All);
let accounts: HashSet<String> = list
.split_whitespace()
.filter(|v| v.starts_with("npub1") && !v.ends_with("Lume"))
.map(String::from)
.collect();
accounts.into_iter().collect()
}
pub async fn process_event(client: &Client, events: Events) -> Vec<RichEvent> {
// Remove event thread if event is TextNote
let events: Vec<Event> = events
@@ -201,38 +227,6 @@ pub async fn process_event(client: &Client, events: Events) -> Vec<RichEvent> {
join_all(futures).await
}
pub async fn init_nip65(client: &Client, public_key: &str) {
let author = PublicKey::from_str(public_key).unwrap();
let filter = Filter::new().author(author).kind(Kind::RelayList).limit(1);
// client.add_relay("ws://127.0.0.1:1984").await.unwrap();
// client.connect_relay("ws://127.0.0.1:1984").await.unwrap();
if let Ok(events) = client
.fetch_events(vec![filter], 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.pool().add_relay(&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);
}
}
}
}
}
pub async fn parse_event(content: &str) -> Meta {
let mut finder = LinkFinder::new();
finder.url_must_have_scheme(false);

View File

@@ -5,14 +5,20 @@
#[cfg(target_os = "macos")]
use border::WebviewWindowExt as BorderWebviewWindowExt;
use commands::{account::*, event::*, metadata::*, relay::*, window::*};
use common::parse_event;
use commands::{
account::*,
event::*,
metadata::*,
relay::*,
sync::{run_fast_sync, NegentropyEvent},
window::*,
};
use common::{get_all_accounts, get_tags_content, parse_event};
use nostr_sdk::prelude::{Profile as DatabaseProfile, *};
use serde::{Deserialize, Serialize};
use specta::Type;
use specta_typescript::Typescript;
use std::{
collections::HashSet,
fs,
io::{self, BufRead},
str::FromStr,
@@ -30,8 +36,9 @@ pub mod common;
pub struct Nostr {
client: Client,
settings: Mutex<Settings>,
contact_list: Mutex<Vec<Contact>>,
trusted_list: Mutex<HashSet<PublicKey>>,
accounts: Mutex<Vec<String>>,
subscriptions: Mutex<Vec<SubscriptionId>>,
bootstrap_relays: Mutex<Vec<Url>>,
}
#[derive(Clone, Serialize, Deserialize, Type)]
@@ -76,14 +83,14 @@ struct Subscription {
label: String,
kind: SubKind,
event_id: Option<String>,
contacts: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Type, Clone, TauriEvent)]
struct NewSettings(Settings);
pub const DEFAULT_DIFFICULTY: u8 = 21;
pub const FETCH_LIMIT: usize = 100;
pub const NOTIFICATION_NEG_LIMIT: usize = 64;
pub const FETCH_LIMIT: usize = 50;
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
fn main() {
@@ -99,22 +106,21 @@ fn main() {
get_bootstrap_relays,
save_bootstrap_relays,
get_accounts,
create_account,
watch_account,
import_account,
connect_account,
get_private_key,
delete_account,
reset_password,
is_account_sync,
create_sync_file,
login,
has_signer,
set_signer,
get_profile,
set_profile,
get_contact_list,
set_contact_list,
check_contact,
is_contact,
toggle_contact,
get_mention_list,
get_all_profiles,
set_group,
get_group,
get_all_groups,
@@ -131,7 +137,6 @@ fn main() {
get_user_settings,
set_user_settings,
verify_nip05,
is_trusted_user,
get_event_meta,
get_event,
get_event_from,
@@ -142,12 +147,13 @@ fn main() {
get_all_events_by_hashtags,
get_local_events,
get_global_events,
is_deleted_event,
request_delete,
search,
publish,
reply,
repost,
is_reposted,
request_delete,
is_deleted_event,
event_to_bech32,
user_to_bech32,
create_column,
@@ -158,7 +164,7 @@ fn main() {
reopen_lume,
quit
])
.events(collect_events![Subscription, NewSettings]);
.events(collect_events![Subscription, NewSettings, NegentropyEvent]);
#[cfg(debug_assertions)]
builder
@@ -179,6 +185,7 @@ fn main() {
let handle = app.handle();
let handle_clone = handle.clone();
let handle_clone_child = handle_clone.clone();
let handle_clone_sync = handle_clone_child.clone();
let main_window = app.get_webview_window("main").unwrap();
let config_dir = handle
@@ -216,7 +223,7 @@ fn main() {
}
});
let client = tauri::async_runtime::block_on(async move {
let (client, bootstrap_relays) = tauri::async_runtime::block_on(async move {
// Setup database
let database = NostrLMDB::open(config_dir.join("nostr-lmdb"))
.expect("Error: cannot create database.");
@@ -224,10 +231,10 @@ fn main() {
// Config
let opts = Options::new()
.gossip(true)
.max_avg_latency(Duration::from_millis(500))
.max_avg_latency(Duration::from_millis(800))
.automatic_authentication(false)
.connection_timeout(Some(Duration::from_secs(20)))
.send_timeout(Some(Duration::from_secs(10)))
.send_timeout(Some(Duration::from_secs(20)))
.timeout(Duration::from_secs(20));
// Setup nostr client
@@ -279,17 +286,27 @@ fn main() {
// Connect
client.connect_with_timeout(Duration::from_secs(10)).await;
client
// Get all bootstrap relays
let bootstrap_relays: Vec<Url> =
client.pool().all_relays().await.into_keys().collect();
(client, bootstrap_relays)
});
let accounts = get_all_accounts();
// Run fast sync for all accounts
run_fast_sync(accounts.clone(), handle_clone_sync);
// Create global state
app.manage(Nostr {
client,
accounts: Mutex::new(accounts),
settings: Mutex::new(Settings::default()),
contact_list: Mutex::new(Vec::new()),
trusted_list: Mutex::new(HashSet::new()),
subscriptions: Mutex::new(Vec::new()),
bootstrap_relays: Mutex::new(bootstrap_relays),
});
// Handle subscription
Subscription::listen_any(app, move |event| {
let handle = handle_clone_child.to_owned();
let payload = event.payload;
@@ -302,42 +319,84 @@ fn main() {
SubKind::Subscribe => {
let subscription_id = SubscriptionId::new(payload.label);
match payload.event_id {
Some(id) => {
if !client
.pool()
.subscriptions()
.await
.contains_key(&subscription_id)
{
// Update state
state
.subscriptions
.lock()
.unwrap()
.push(subscription_id.clone());
println!(
"Total subscriptions: {}",
state.subscriptions.lock().unwrap().len()
);
if let Some(id) = payload.event_id {
let event_id = EventId::from_str(&id).unwrap();
let filter =
Filter::new().event(event_id).since(Timestamp::now());
if let Err(e) = client
.subscribe_with_id(subscription_id, vec![filter], None)
.subscribe_with_id(
subscription_id.clone(),
vec![filter],
None,
)
.await
{
println!("Subscription error: {}", e)
}
}
None => {
let contact_list = state.contact_list.lock().unwrap().clone();
if !contact_list.is_empty() {
let authors: Vec<PublicKey> =
contact_list.iter().map(|f| f.public_key).collect();
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.authors(authors)
.since(Timestamp::now());
if let Some(ids) = payload.contacts {
let authors: Vec<PublicKey> = ids
.iter()
.filter_map(|item| {
if let Ok(pk) = PublicKey::from_str(item) {
Some(pk)
} else {
None
}
})
.collect();
if let Err(e) = client
.subscribe_with_id(subscription_id, vec![filter], None)
.await
{
println!("Subscription error: {}", e)
}
if let Err(e) = client
.subscribe_with_id(
subscription_id,
vec![Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.authors(authors)
.since(Timestamp::now())],
None,
)
.await
{
println!("Subscription error: {}", e)
}
}
};
}
}
SubKind::Unsubscribe => {
let subscription_id = SubscriptionId::new(payload.label);
let mut sub_state = state.subscriptions.lock().unwrap().clone();
if let Some(pos) = sub_state.iter().position(|x| *x == subscription_id)
{
sub_state.remove(pos);
state.subscriptions.lock().unwrap().clone_from(&sub_state)
}
println!(
"Total subscriptions: {}",
state.subscriptions.lock().unwrap().len()
);
client.unsubscribe(subscription_id).await
}
}
@@ -363,6 +422,30 @@ fn main() {
tauri::async_runtime::spawn(async move {
let state = handle_clone.state::<Nostr>();
let client = &state.client;
let accounts = state.accounts.lock().unwrap().clone();
let public_keys: Vec<PublicKey> = accounts
.iter()
.filter_map(|acc| {
if let Ok(pk) = PublicKey::from_str(acc) {
Some(pk)
} else {
None
}
})
.collect();
// Subscribe for new notification
if let Ok(e) = client
.subscribe_with_id(
SubscriptionId::new(NOTIFICATION_SUB_ID),
vec![Filter::new().pubkeys(public_keys).since(Timestamp::now())],
None,
)
.await
{
println!("Subscribed for notification on {} relays", e.success.len())
}
let allow_notification = match handle_clone.notification().request_permission() {
Ok(_) => {
@@ -377,6 +460,7 @@ fn main() {
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
let mut notifications = client.pool().notifications();
let mut new_events: Vec<EventId> = Vec::new();
while let Ok(notification) = notifications.recv().await {
match notification {
@@ -394,6 +478,17 @@ fn main() {
println!("Error: {}", e);
}
// Workaround for https://github.com/rust-nostr/nostr/issues/509
// TODO: remove
let _ = client
.fetch_events(
vec![Filter::new()
.kind(Kind::TextNote)
.limit(0)],
Some(Duration::from_secs(5)),
)
.await;
if allow_notification {
if let Err(e) = &handle_clone
.notification()
@@ -426,8 +521,12 @@ fn main() {
event,
} = message
{
let tags = get_tags_content(&event, TagKind::p());
// Handle events from notification subscription
if subscription_id == notification_id {
if subscription_id == notification_id
&& tags.iter().any(|item| accounts.iter().any(|i| i == item))
{
// Send native notification
if allow_notification {
let author = client
@@ -437,27 +536,46 @@ fn main() {
.unwrap_or_else(|_| {
DatabaseProfile::new(event.pubkey, Metadata::new())
});
let metadata = author.metadata();
send_event_notification(&event, metadata, &handle_clone);
send_event_notification(
&event,
author.metadata(),
&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
let payload = RichEvent {
raw: event.as_json(),
parsed: if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await)
} else {
None
},
};
handle_clone
.emit_to(
EventTarget::labeled(label),
EventTarget::labeled(subscription_id.to_string()),
"event",
RichEvent { raw, parsed },
payload,
)
.unwrap();
if state
.subscriptions
.lock()
.unwrap()
.iter()
.any(|i| i == &subscription_id)
{
new_events.push(event.id);
if new_events.len() > 5 {
handle_clone.emit("synchronized", ()).unwrap();
new_events.clear();
}
}
};
}
RelayPoolNotification::Shutdown => break,
@@ -468,47 +586,6 @@ fn main() {
Ok(())
})
.on_window_event(|window, event| {
if let tauri::WindowEvent::Focused(focused) = event {
if !focused {
let handle = window.app_handle().to_owned();
let config_dir = handle.path().app_config_dir().unwrap();
tauri::async_runtime::spawn(async move {
let state = handle.state::<Nostr>();
let client = &state.client;
if let Ok(signer) = client.signer().await {
let public_key = signer.public_key().await.unwrap();
let bech32 = public_key.to_bech32().unwrap();
if fs::metadata(config_dir.join(bech32)).is_ok() {
if let Ok(contact_list) =
client.get_contact_list(Some(Duration::from_secs(5))).await
{
let authors: Vec<PublicKey> =
contact_list.iter().map(|f| f.public_key).collect();
if client
.reconcile(
Filter::new()
.authors(authors)
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(1000),
NegentropyOptions::default(),
)
.await
.is_ok()
{
handle.emit("synchronized", ()).unwrap();
}
}
}
}
});
}
}
})
.plugin(prevent_default())
.plugin(tauri_plugin_decorum::init())
.plugin(tauri_plugin_store::Builder::default().build())