diff --git a/public/nosta.jpg b/public/nosta.jpg new file mode 100644 index 00000000..961c45e7 Binary files /dev/null and b/public/nosta.jpg differ diff --git a/public/nsec_app.svg b/public/nsec_app.svg new file mode 100644 index 00000000..ac1187fb --- /dev/null +++ b/public/nsec_app.svg @@ -0,0 +1,3 @@ + + + diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 57f7fd55..f6f4d1a7 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -11,10 +11,7 @@ use std::{ }; use tauri::{Emitter, Manager, State}; -use crate::{ - common::{get_user_settings, init_nip65}, - Nostr, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT, NOTIFICATION_SUB_ID, -}; +use crate::{common::init_nip65, Nostr, NOTIFICATION_SUB_ID}; #[derive(Debug, Clone, Serialize, Deserialize, Type)] struct Account { @@ -271,104 +268,138 @@ pub async fn login( } }; - // Connect to user's relay (NIP-65) + // NIP-65: Connect to user's relay list init_nip65(client, &public_key).await; + // Run seperate thread for syncing data + let pk = public_key.clone(); tauri::async_runtime::spawn(async move { - let config_dir = handle - .path() - .app_config_dir() - .expect("Error: app config directory not found."); - + let config_dir = handle.path().app_config_dir().unwrap(); let state = handle.state::(); let client = &state.client; - let signer = client.signer().await.unwrap(); - let public_key = signer.public_key().await.unwrap(); + // Convert current user to PublicKey + let author = PublicKey::from_str(&pk).unwrap(); - let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID); - let notification = Filter::new().pubkey(public_key).kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]); - - // Sync notification with negentropy - let _ = client + // Fetching user's metadata + if let Ok(report) = client .reconcile( - notification.clone().limit(NOTIFICATION_NEG_LIMIT), + Filter::new() + .author(author) + .kinds(vec![ + Kind::Metadata, + Kind::ContactList, + Kind::MuteList, + Kind::Bookmarks, + Kind::Interests, + Kind::PinList, + ]) + .limit(1000), NegentropyOptions::default(), ) - .await; + .await + { + println!("Received: {}", report.received.len()) + } - // Subscribing for new notification... - if let Err(e) = client + // Fetching user's events + if let Ok(report) = client + .reconcile( + Filter::new() + .author(author) + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(200), + NegentropyOptions::default(), + ) + .await + { + println!("Received: {}", report.received.len()) + } + + // Fetching user's notification + if let Ok(report) = client + .reconcile( + Filter::new() + .pubkey(author) + .kinds(vec![ + Kind::TextNote, + Kind::Repost, + Kind::Reaction, + Kind::ZapReceipt, + ]) + .limit(200), + NegentropyOptions::default(), + ) + .await + { + println!("Received: {}", report.received.len()) + } + + // Subscribe for new notification + if let Ok(e) = client .subscribe_with_id( - notification_id, - vec![notification.since(Timestamp::now())], + SubscriptionId::new(NOTIFICATION_SUB_ID), + vec![Filter::new() + .pubkey(author) + .kinds(vec![ + Kind::TextNote, + Kind::Repost, + Kind::Reaction, + Kind::ZapReceipt, + ]) + .since(Timestamp::now())], None, ) .await { - println!("Error: {}", e) + println!("Subscribed: {}", e.success.len()) } - // Get user's settings - if let Ok(settings) = get_user_settings(client).await { - state.settings.lock().await.clone_from(&settings); + // Get user's contact list + let contact_list = { + let contacts = client.get_contact_list(None).await.unwrap(); + // Update app's state + state.contact_list.lock().await.clone_from(&contacts); + // Return + contacts }; - let contact_list = client - .get_contact_list(Some(Duration::from_secs(5))) - .await - .unwrap(); - - state.contact_list.lock().await.clone_from(&contact_list); - - // Get user's contact list + // Get events from contact list if !contact_list.is_empty() { let authors: Vec = contact_list.iter().map(|f| f.public_key).collect(); - let metadata = Filter::new() - .authors(authors.clone()) - .kind(Kind::Metadata) - .limit(authors.len()); - + // Fetching contact's metadata if let Ok(report) = client - .reconcile(metadata, NegentropyOptions::default()) + .reconcile( + Filter::new() + .authors(authors.clone()) + .kinds(vec![Kind::Metadata, Kind::ContactList, Kind::MuteList]) + .limit(3000), + NegentropyOptions::default(), + ) .await { - println!("received [metadata]: {}", report.received.len()) + println!("Received: {}", report.received.len()) } - let newsfeed = Filter::new() - .authors(authors.clone()) - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(NEWSFEED_NEG_LIMIT); - - if client - .reconcile(newsfeed, NegentropyOptions::default()) + // Fetching contact's events + if let Ok(report) = client + .reconcile( + Filter::new() + .authors(authors.clone()) + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(1000), + NegentropyOptions::default(), + ) .await - .is_ok() { - // Save state - let _ = File::create(config_dir.join(public_key.to_bech32().unwrap())); + println!("Received: {}", report.received.len()); + + // Save the process status + let _ = File::create(config_dir.join(author.to_bech32().unwrap())); // Update frontend handle.emit("synchronized", ()).unwrap(); - } - - let contacts = Filter::new() - .authors(authors.clone()) - .kind(Kind::ContactList) - .limit(authors.len() * 1000); - - if let Ok(report) = client - .reconcile(contacts, NegentropyOptions::default()) - .await - { - println!("received [contact list]: {}", report.received.len()) - } + }; for author in authors.into_iter() { let filter = Filter::new() @@ -400,7 +431,7 @@ pub async fn login( } } else { handle.emit("synchronized", ()).unwrap(); - }; + } }); Ok(public_key) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9c6d8b17..249953c7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -478,13 +478,15 @@ fn main() { { let authors: Vec = contact_list.iter().map(|f| f.public_key).collect(); - let newsfeed = Filter::new() - .authors(authors) - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(NEWSFEED_NEG_LIMIT); if client - .reconcile(newsfeed, NegentropyOptions::default()) + .reconcile( + Filter::new() + .authors(authors) + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(NEWSFEED_NEG_LIMIT), + NegentropyOptions::default(), + ) .await .is_ok() { diff --git a/src/components/note/buttons/open.tsx b/src/components/note/buttons/open.tsx index 06387861..9eb18333 100644 --- a/src/components/note/buttons/open.tsx +++ b/src/components/note/buttons/open.tsx @@ -20,7 +20,7 @@ export function NoteOpenThread() { - Open + View thread diff --git a/src/components/user/avatar.tsx b/src/components/user/avatar.tsx index aab475c4..de602242 100644 --- a/src/components/user/avatar.tsx +++ b/src/components/user/avatar.tsx @@ -15,6 +15,9 @@ export function UserAvatar({ className }: { className?: string }) { const picture = useMemo(() => { if (service?.length && user.profile?.picture?.length) { + if (user.profile?.picture.includes("_next/")) { + return user.profile?.picture; + } return `${service}?url=${user.profile?.picture}&w=100&h=100&n=-1&default=${user.profile?.picture}`; } else { return user.profile?.picture; diff --git a/src/routes/index.lazy.tsx b/src/routes/index.lazy.tsx index d01d281d..7851ce14 100644 --- a/src/routes/index.lazy.tsx +++ b/src/routes/index.lazy.tsx @@ -2,7 +2,7 @@ import { commands } from "@/commands.gen"; import { appSettings, displayNpub } from "@/commons"; import { Frame, Spinner, User } from "@/components"; import { ArrowRight, DotsThree, GearSix, Plus } from "@phosphor-icons/react"; -import { Link, createLazyFileRoute } from "@tanstack/react-router"; +import { createLazyFileRoute } from "@tanstack/react-router"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { message } from "@tauri-apps/plugin-dialog"; import { @@ -37,6 +37,32 @@ function Screen() { const [password, setPassword] = useState(""); const [isPending, startTransition] = useTransition(); + const showContextMenu = useCallback( + async (e: React.MouseEvent, account: string) => { + e.stopPropagation(); + + const menuItems = await Promise.all([ + MenuItem.new({ + text: "Reset password", + enabled: !account.includes("_nostrconnect"), + // @ts-ignore, this is tanstack router bug + action: () => navigate({ to: "/reset", search: { account } }), + }), + MenuItem.new({ + text: "Delete account", + action: async () => await deleteAccount(account), + }), + ]); + + const menu = await Menu.new({ + items: menuItems, + }); + + await menu.popup().catch((e) => console.error(e)); + }, + [], + ); + const deleteAccount = async (account: string) => { const res = await commands.deleteAccount(account); @@ -69,12 +95,14 @@ function Screen() { if (status) { navigate({ to: "/$account/home", + // @ts-ignore, this is tanstack router bug params: { account: res.data }, replace: true, }); } else { navigate({ to: "/loading", + // @ts-ignore, this is tanstack router bug search: { account: res.data }, replace: true, }); @@ -86,31 +114,6 @@ function Screen() { }); }; - const showContextMenu = useCallback( - async (e: React.MouseEvent, account: string) => { - e.stopPropagation(); - - const menuItems = await Promise.all([ - MenuItem.new({ - text: "Reset password", - enabled: !account.includes("_nostrconnect"), - action: () => navigate({ to: "/reset", search: { account } }), - }), - MenuItem.new({ - text: "Delete account", - action: async () => await deleteAccount(account), - }), - ]); - - const menu = await Menu.new({ - items: menuItems, - }); - - await menu.popup().catch((e) => console.error(e)); - }, - [], - ); - useEffect(() => { if (autoLogin) { loginWith(); @@ -204,8 +207,8 @@ function Screen() { ))} -
@@ -216,17 +219,17 @@ function Screen() { New account
- +
- Manage Relays - +
); diff --git a/src/routes/loading.tsx b/src/routes/loading.tsx index d6d5d22c..358b3bf3 100644 --- a/src/routes/loading.tsx +++ b/src/routes/loading.tsx @@ -1,4 +1,4 @@ -import { Spinner } from "@/components"; +import { Frame, Spinner } from "@/components"; import { createFileRoute } from "@tanstack/react-router"; import { listen } from "@tauri-apps/api/event"; import { useEffect } from "react"; @@ -24,6 +24,7 @@ function Screen() { const unlisten = listen("synchronized", () => { navigate({ to: "/$account/home", + // @ts-ignore, this is tanstack router bug params: { account: search.account }, replace: true, }); @@ -36,12 +37,15 @@ function Screen() { return (
-
+ -

+

Fetching necessary data for the first time login...

-
+
); } diff --git a/src/routes/new.lazy.tsx b/src/routes/new.lazy.tsx index d4129890..209c5526 100644 --- a/src/routes/new.lazy.tsx +++ b/src/routes/new.lazy.tsx @@ -1,6 +1,4 @@ -import { GoBack } from "@/components"; -import { ArrowLeft } from "@phosphor-icons/react"; -import { Link, createLazyFileRoute } from "@tanstack/react-router"; +import { createLazyFileRoute } from "@tanstack/react-router"; export const Route = createLazyFileRoute("/new")({ component: Screen, @@ -10,42 +8,79 @@ function Screen() { return (
-
+
-

- Welcome to Nostr. +

+ How would you like to use Lume?

- - Create a new identity - -
+

Continue with Nostr Connect

+
+

+ Your account will be handled by a remote signer. Lume will not + store your account keys. +

+
+ + +

Continue with Secret Key

+
+

+ Lume will store your keys in secure storage. You can provide a + password to add extra security. +

+
+
+
+
+
+ Do you not have a Nostr account yet? +
+
+
- - Login with Nostr Connect - - + nsec.app +
+ Create one with nsec.app + + - Login with Private Key - +
+ nosta +
+ Create one with nosta.me +
+

+ Or you can create account from other Nostr clients. +

- - - Back -
); }