clean up & save onboarding process as state

This commit is contained in:
Ren Amamiya
2023-08-10 12:34:11 +07:00
parent d63690e9d0
commit e6d8f084ae
38 changed files with 545 additions and 695 deletions

View File

@@ -40,6 +40,12 @@ const appLoader = async () => {
const account = await getActiveAccount();
const stronghold = sessionStorage.getItem('stronghold');
const privkey = JSON.parse(stronghold).state.privkey || null;
const onboarding = localStorage.getItem('onboarding');
const step = JSON.parse(onboarding).state.step || null;
if (step) {
return redirect(step);
}
if (!account) {
return redirect('/auth/welcome');

View File

@@ -1,6 +1,19 @@
import { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import { useOnboarding } from '@stores/onboarding';
import { useStronghold } from '@stores/stronghold';
export function AuthCreateScreen() {
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
const setPrivkey = useStronghold((state) => state.setPrivkey);
useEffect(() => {
if (step) {
setPrivkey(tmpPrivkey);
}
}, [tmpPrivkey]);
return (
<div className="flex h-full w-full items-center justify-center">
<Outlet />

View File

@@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { BaseDirectory, writeTextFile } from '@tauri-apps/plugin-fs';
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { createAccount } from '@libs/storage';
@@ -17,7 +17,9 @@ export function CreateStep1Screen() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const setPrivkey = useStronghold((state) => state.setPrivkey);
const setTempPrivkey = useOnboarding((state) => state.setTempPrivkey);
const setPubkey = useOnboarding((state) => state.setPubkey);
const setStep = useOnboarding((state) => state.setStep);
const [privkeyInput, setPrivkeyInput] = useState('password');
const [loading, setLoading] = useState(false);
@@ -61,8 +63,9 @@ export function CreateStep1Screen() {
const submit = () => {
setLoading(true);
setPubkey(pubkey);
setPrivkey(privkey);
setTempPrivkey(privkey); // only use if user close app and reopen it
setPubkey(pubkey);
account.mutate({
npub,
@@ -75,6 +78,11 @@ export function CreateStep1Screen() {
setTimeout(() => navigate('/auth/create/step-2', { replace: true }), 1200);
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/create/step-1');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
@@ -30,13 +30,13 @@ const resolver: Resolver<FormValues> = async (values) => {
export function CreateStep2Screen() {
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const pubkey = useOnboarding((state) => state.pubkey);
const privkey = useStronghold((state) => state.privkey);
const [passwordInput, setPasswordInput] = useState('password');
const [loading, setLoading] = useState(false);
const privkey = useStronghold((state) => state.privkey);
const pubkey = useOnboarding((state) => state.pubkey);
const { save } = useSecureStorage();
// toggle private key
@@ -72,6 +72,11 @@ export function CreateStep2Screen() {
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/create/step-2');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">

View File

@@ -1,5 +1,5 @@
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
@@ -10,19 +10,20 @@ import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useOnboarding } from '@stores/onboarding';
import { usePublish } from '@utils/hooks/usePublish';
import { useNostr } from '@utils/hooks/useNostr';
export function CreateStep3Screen() {
const { publish } = usePublish();
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const [picture, setPicture] = useState(DEFAULT_AVATAR);
const [banner, setBanner] = useState('');
const { publish } = useNostr();
const {
register,
handleSubmit,
@@ -57,6 +58,11 @@ export function CreateStep3Screen() {
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/create/step-3');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">

View File

@@ -1,6 +1,19 @@
import { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import { useOnboarding } from '@stores/onboarding';
import { useStronghold } from '@stores/stronghold';
export function AuthImportScreen() {
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
const setPrivkey = useStronghold((state) => state.setPrivkey);
useEffect(() => {
if (step) {
setPrivkey(tmpPrivkey);
}
}, [tmpPrivkey]);
return (
<div className="flex h-full w-full items-center justify-center">
<Outlet />

View File

@@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { getPublicKey, nip19 } from 'nostr-tools';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
@@ -34,7 +34,9 @@ export function ImportStep1Screen() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const setPrivkey = useStronghold((state) => state.setPrivkey);
const setTempPubkey = useOnboarding((state) => state.setTempPrivkey);
const setPubkey = useOnboarding((state) => state.setPubkey);
const setStep = useOnboarding((state) => state.setStep);
const [loading, setLoading] = useState(false);
@@ -72,10 +74,9 @@ export function ImportStep1Screen() {
const pubkey = getPublicKey(privkey);
const npub = nip19.npubEncode(pubkey);
// use for onboarding process only
setPubkey(pubkey);
// add stronghold state
setPrivkey(privkey);
setTempPubkey(privkey); // only use if user close app and reopen it
setPubkey(pubkey);
// add account to local database
account.mutate({
@@ -91,11 +92,16 @@ export function ImportStep1Screen() {
} catch (error) {
setError('privkey', {
type: 'custom',
message: 'Private Key is invalid, please check again',
message: 'Private key is invalid, please check again',
});
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/import/step-1');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
@@ -30,13 +30,13 @@ const resolver: Resolver<FormValues> = async (values) => {
export function ImportStep2Screen() {
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const pubkey = useOnboarding((state) => state.pubkey);
const privkey = useStronghold((state) => state.privkey);
const [passwordInput, setPasswordInput] = useState('password');
const [loading, setLoading] = useState(false);
const privkey = useStronghold((state) => state.privkey);
const pubkey = useOnboarding((state) => state.pubkey);
const { save } = useSecureStorage();
// toggle private key
@@ -72,6 +72,11 @@ export function ImportStep2Screen() {
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/import/step-2');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">

View File

@@ -1,5 +1,5 @@
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
@@ -8,12 +8,15 @@ import { updateLastLogin } from '@libs/storage';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
import { useAccount } from '@utils/hooks/useAccount';
import { useNostr } from '@utils/hooks/useNostr';
export function ImportStep3Screen() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const [loading, setLoading] = useState(false);
@@ -39,6 +42,11 @@ export function ImportStep3Screen() {
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/import/step-3');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">

View File

@@ -1,6 +1,19 @@
import { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import { useOnboarding } from '@stores/onboarding';
import { useStronghold } from '@stores/stronghold';
export function OnboardingScreen() {
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
const setPrivkey = useStronghold((state) => state.setPrivkey);
useEffect(() => {
if (step) {
setPrivkey(tmpPrivkey);
}
}, [tmpPrivkey]);
return (
<div className="flex h-full w-full items-center justify-center">
<Outlet />

View File

@@ -1,5 +1,5 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
@@ -8,17 +8,18 @@ import { updateAccount } from '@libs/storage';
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
import { useAccount } from '@utils/hooks/useAccount';
import { useNostr } from '@utils/hooks/useNostr';
import { usePublish } from '@utils/hooks/usePublish';
import { arrayToNIP02 } from '@utils/transform';
export function OnboardStep1Screen() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const { publish } = usePublish();
const { fetchNotes } = useNostr();
const { publish, fetchNotes } = useNostr();
const { account } = useAccount();
const { status, data } = useQuery(['trending-profiles'], async () => {
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
@@ -62,6 +63,11 @@ export function OnboardStep1Screen() {
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/onboarding');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { createBlock } from '@libs/storage';
@@ -6,6 +6,7 @@ import { createBlock } from '@libs/storage';
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { BLOCK_KINDS } from '@stores/constants';
import { useOnboarding } from '@stores/onboarding';
const data = [
{ hashtag: '#bitcoin' },
@@ -27,6 +28,7 @@ const data = [
export function OnboardStep2Screen() {
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const [loading, setLoading] = useState(false);
const [tags, setTags] = useState(new Set<string>());
@@ -57,6 +59,11 @@ export function OnboardStep2Screen() {
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/onboarding/step-2');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">

View File

@@ -10,17 +10,19 @@ import { createRelay } from '@libs/storage';
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { FULL_RELAYS } from '@stores/constants';
import { useOnboarding } from '@stores/onboarding';
import { useAccount } from '@utils/hooks/useAccount';
import { usePublish } from '@utils/hooks/usePublish';
import { useNostr } from '@utils/hooks/useNostr';
export function OnboardStep3Screen() {
const navigate = useNavigate();
const [setStep, clearStep] = useOnboarding((state) => [state.setStep, state.clearStep]);
const [loading, setLoading] = useState(false);
const [relays, setRelays] = useState(new Set<string>());
const { publish } = usePublish();
const { publish } = useNostr();
const { account } = useAccount();
const { fetcher, relayUrls } = useNDK();
const { status, data } = useQuery(
@@ -48,6 +50,9 @@ export function OnboardStep3Screen() {
}
);
// save current step, if user close app and reopen it
setStep('/auth/onboarding/step-3');
const toggleRelay = (relay: string) => {
if (relays.has(relay)) {
setRelays((prev) => {
@@ -76,6 +81,7 @@ export function OnboardStep3Screen() {
}
setTimeout(() => {
clearStep();
navigate('/', { replace: true });
}, 1000);
} catch (e) {

View File

@@ -9,7 +9,9 @@ export function WelcomeScreen() {
async function setWindow() {
await appWindow.setSize(new LogicalSize(400, 500));
await appWindow.setResizable(false);
await appWindow.center();
}
setWindow();
return () => {

View File

@@ -4,7 +4,7 @@ import { useCallback, useState } from 'react';
import { EnterIcon } from '@shared/icons';
import { MediaUploader } from '@shared/mediaUploader';
import { usePublish } from '@utils/hooks/usePublish';
import { useNostr } from '@utils/hooks/useNostr';
export function ChatMessageForm({
receiverPubkey,
@@ -14,7 +14,7 @@ export function ChatMessageForm({
userPubkey: string;
userPrivkey: string;
}) {
const { publish } = usePublish();
const { publish } = useNostr();
const [value, setValue] = useState('');
const encryptMessage = useCallback(async () => {

View File

@@ -12,14 +12,14 @@ import { Image } from '@shared/image';
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
import { usePublish } from '@utils/hooks/usePublish';
import { useNostr } from '@utils/hooks/useNostr';
import { useImageUploader } from '@utils/hooks/useUploader';
export function ImageModal() {
const queryClient = useQueryClient();
const upload = useImageUploader();
const { publish } = usePublish();
const { publish } = useNostr();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);

View File

@@ -4,8 +4,6 @@ import { useEffect, useRef } from 'react';
import { useNDK } from '@libs/ndk/provider';
import { createNote } from '@libs/storage';
import { useNote } from '@stores/note';
import { useAccount } from '@utils/hooks/useAccount';
export function useNewsfeed() {
@@ -15,8 +13,6 @@ export function useNewsfeed() {
const { ndk } = useNDK();
const { status, account } = useAccount();
const toggleHasNewNote = useNote((state) => state.toggleHasNewNote);
useEffect(() => {
if (status === 'success' && account) {
const filter: NDKFilter = {
@@ -37,8 +33,6 @@ export function useNewsfeed() {
event.content,
event.created_at
);
// notify user about created note
toggleHasNewNote(true);
});
}

View File

@@ -1,7 +1,5 @@
import { invoke } from '@tauri-apps/api/tauri';
import { platform } from '@tauri-apps/plugin-os';
import { appWindow } from '@tauri-apps/plugin-window';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { getActiveAccount, updateLastLogin } from '@libs/storage';
@@ -10,47 +8,76 @@ import { LoaderIcon } from '@shared/icons';
import { useNostr } from '@utils/hooks/useNostr';
const account = await getActiveAccount();
const osPlatform = await platform();
if (osPlatform !== 'macos') {
appWindow.setDecorations(false);
}
export function SplashScreen() {
const [loading, setLoading] = useState(true);
const { fetchChats, fetchNotes } = useNostr();
if (!account) {
setTimeout(async () => await invoke('close_splashscreen'), 500);
}
const skip = async () => {
await invoke('close_splashscreen');
};
useEffect(() => {
async function prefetch() {
const onboarding = localStorage.getItem('onboarding');
const step = JSON.parse(onboarding).state.step || null;
if (step) await invoke('close_splashscreen');
const notes = await fetchNotes();
const chats = await fetchChats();
if (notes && chats) {
if (notes.status === 'ok' && chats.status === 'ok') {
const now = Math.floor(Date.now() / 1000);
await updateLastLogin(now);
invoke('close_splashscreen');
} else {
setLoading(false);
console.log('fetch notes failed, error: ', notes.message);
console.log('fetch chats failed, error: ', chats.message);
}
}
if (account) {
if (account && loading) {
prefetch();
}
}, []);
if (!account) {
setTimeout(() => invoke('close_splashscreen'), 1000);
}
return (
<div className="relative flex h-screen w-screen items-center justify-center bg-black">
<div data-tauri-drag-region className="absolute left-0 top-0 z-10 h-11 w-full" />
<div className="flex min-h-0 w-full flex-1 items-center justify-center">
<div className="flex flex-col items-center justify-center gap-4">
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
<div className="flex flex-col gap-1 text-center">
<h3 className="font-semibold leading-none text-white">Prefetching data</h3>
<p className="text-sm leading-none text-white/50">
This may take a few seconds, please don&apos;t close app.
</p>
</div>
{loading ? (
<div className="mt-2 flex flex-col gap-1 text-center">
<h3 className="text-lg font-semibold leading-none text-white">
Prefetching data
</h3>
<p className="text-white/50">
This may take a few seconds, please don&apos;t close app.
</p>
</div>
) : (
<div className="mt-2 flex flex-col gap-1 text-center">
<h3 className="text-lg font-semibold leading-none text-white">
Something wrong!
</h3>
<p className="text-white/50">
Prefetching process failed, click skip to continue.
</p>
<button
type="button"
onClick={skip}
className="mx-auto mt-4 inline-flex h-10 w-max items-center justify-center rounded-md bg-white/10 px-8 text-sm font-medium leading-none text-white backdrop-blur-xl hover:bg-white/20"
>
Skip
</button>
</div>
)}
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
// source: https://github.com/nostr-dev-kit/ndk-react/
// inspire by: https://github.com/nostr-dev-kit/ndk-react/
import NDK from '@nostr-dev-kit/ndk';
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
import { NostrFetcher } from 'nostr-fetch';
@@ -10,21 +10,16 @@ import { getExplicitRelayUrls } from '@libs/storage';
import { FULL_RELAYS } from '@stores/constants';
export const NDKInstance = () => {
const cacheAdapter = useMemo(() => new TauriAdapter(), []);
const [ndk, setNDK] = useState<NDK | undefined>(undefined);
const [relayUrls, setRelayUrls] = useState<string[]>([]);
const [fetcher, setFetcher] = useState<NostrFetcher>(undefined);
useEffect(() => {
if (!ndk) loadNdk();
const cacheAdapter = useMemo(() => new TauriAdapter(), []);
const fetcher = useMemo<NostrFetcher>(
() => NostrFetcher.withCustomPool(ndkAdapter(ndk)),
[ndk]
);
return () => {
cacheAdapter.save();
};
}, []);
async function loadNdk() {
async function initNDK() {
let explicitRelayUrls: string[];
const explicitRelayUrlsFromDB = await getExplicitRelayUrls();
@@ -34,23 +29,29 @@ export const NDKInstance = () => {
explicitRelayUrls = FULL_RELAYS;
}
const ndkInstance = new NDK({ explicitRelayUrls, cacheAdapter });
const instance = new NDK({ explicitRelayUrls, cacheAdapter });
try {
await ndkInstance.connect();
await instance.connect();
} catch (error) {
console.error('ERROR loading NDK NDKInstance', error);
console.error('NDK instance init failed: ', error);
}
setNDK(ndkInstance);
setNDK(instance);
setRelayUrls(explicitRelayUrls);
setFetcher(NostrFetcher.withCustomPool(ndkAdapter(ndkInstance)));
}
useEffect(() => {
if (!ndk) initNDK();
return () => {
cacheAdapter.save();
};
}, []);
return {
ndk,
relayUrls,
fetcher,
loadNdk,
};
};

View File

@@ -9,18 +9,16 @@ interface NDKContext {
ndk: NDK;
relayUrls: string[];
fetcher: NostrFetcher;
loadNdk: () => void;
}
const NDKContext = createContext<NDKContext>({
ndk: new NDK({}),
relayUrls: [],
fetcher: undefined,
loadNdk: undefined,
});
const NDKProvider = ({ children }: PropsWithChildren<object>) => {
const { ndk, relayUrls, fetcher, loadNdk } = NDKInstance();
const { ndk, relayUrls, fetcher } = NDKInstance();
if (ndk)
return (
@@ -29,7 +27,6 @@ const NDKProvider = ({ children }: PropsWithChildren<object>) => {
ndk,
relayUrls,
fetcher,
loadNdk,
}}
>
{children}

View File

@@ -15,12 +15,12 @@ import { MentionNote } from '@shared/notes';
import { useComposer } from '@stores/composer';
import { usePublish } from '@utils/hooks/usePublish';
import { useNostr } from '@utils/hooks/useNostr';
import { useImageUploader } from '@utils/hooks/useUploader';
import { sendNativeNotification } from '@utils/notification';
export function Composer() {
const { publish } = usePublish();
const { publish } = useNostr();
const [status, setStatus] = useState<null | 'loading' | 'done'>(null);
const [reply, clearReply] = useComposer((state) => [state.reply, state.clearReply]);

View File

@@ -13,7 +13,7 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useAccount } from '@utils/hooks/useAccount';
import { usePublish } from '@utils/hooks/usePublish';
import { useNostr } from '@utils/hooks/useNostr';
export function EditProfileModal() {
const queryClient = useQueryClient();
@@ -24,7 +24,7 @@ export function EditProfileModal() {
const [banner, setBanner] = useState('');
const [nip05, setNIP05] = useState({ verified: false, text: '' });
const { publish } = usePublish();
const { publish } = useNostr();
const { account } = useAccount();
const {
register,
@@ -65,7 +65,6 @@ export function EditProfileModal() {
const res: any = await fetch(verifyURL, {
method: 'GET',
timeout: 30,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},

View File

@@ -3,7 +3,7 @@ import { useState } from 'react';
import { ReactionIcon } from '@shared/icons';
import { usePublish } from '@utils/hooks/usePublish';
import { useNostr } from '@utils/hooks/useNostr';
const REACTIONS = [
{
@@ -32,7 +32,7 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
const [open, setOpen] = useState(false);
const [reaction, setReaction] = useState<string | null>(null);
const { publish } = usePublish();
const { publish } = useNostr();
const getReactionImage = (content: string) => {
const reaction: { img: string } = REACTIONS.find((el) => el.content === content);

View File

@@ -2,16 +2,14 @@ import * as Tooltip from '@radix-ui/react-tooltip';
import { RepostIcon } from '@shared/icons';
import { FULL_RELAYS } from '@stores/constants';
import { usePublish } from '@utils/hooks/usePublish';
import { useNostr } from '@utils/hooks/useNostr';
export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
const { publish } = usePublish();
const { publish } = useNostr();
const submit = async () => {
const tags = [
['e', id, FULL_RELAYS[0], 'root'],
['e', id, 'wss://relayable.org', 'root'],
['p', pubkey],
];
await publish({ content: '', kind: 6, tags: tags });

View File

@@ -8,10 +8,10 @@ import { Button } from '@shared/button';
import { CancelIcon, ZapIcon } from '@shared/icons';
import { useEvent } from '@utils/hooks/useEvent';
import { usePublish } from '@utils/hooks/usePublish';
import { useNostr } from '@utils/hooks/useNostr';
export function NoteZap({ id }: { id: string }) {
const { createZap } = usePublish();
const { createZap } = useNostr();
const { data: event } = useEvent(id);
const [amount, setAmount] = useState<null | number>(null);
@@ -23,6 +23,7 @@ export function NoteZap({ id }: { id: string }) {
};
const createZapRequest = async () => {
// @ts-expect-error, todo: fix this
const res = await createZap(event as unknown as NostrEvent, amount);
if (res) setInvoice(res);
};

View File

@@ -5,12 +5,12 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR, FULL_RELAYS } from '@stores/constants';
import { useNostr } from '@utils/hooks/useNostr';
import { useProfile } from '@utils/hooks/useProfile';
import { usePublish } from '@utils/hooks/usePublish';
import { displayNpub } from '@utils/shortenKey';
export function NoteReplyForm({ id, pubkey }: { id: string; pubkey: string }) {
const { publish } = usePublish();
const { publish } = useNostr();
const { status, user } = useProfile(pubkey);
const [value, setValue] = useState('');

View File

@@ -1,13 +0,0 @@
import { create } from 'zustand';
interface NoteState {
hasNewNote: boolean;
toggleHasNewNote: (status: boolean) => void;
}
export const useNote = create<NoteState>((set) => ({
hasNewNote: false,
toggleHasNewNote: (status: boolean) => {
set({ hasNewNote: status });
},
}));

View File

@@ -1,20 +1,38 @@
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
interface OnboardingState {
profile: { [x: string]: string };
pubkey: string;
createProfile: (data: { [x: string]: string }) => void;
step: null | string;
pubkey: null | string;
tempPrivkey: null | string;
setPubkey: (pubkey: string) => void;
setTempPrivkey: (privkey: string) => void;
setStep: (url: string) => void;
clearStep: () => void;
}
export const useOnboarding = create<OnboardingState>((set) => ({
profile: {},
pubkey: '',
privkey: '',
createProfile: (data: { [x: string]: string }) => {
set({ profile: data });
},
setPubkey: (pubkey: string) => {
set({ pubkey: pubkey });
},
}));
export const useOnboarding = create<OnboardingState>()(
persist(
(set) => ({
step: null,
pubkey: null,
tempPrivkey: null,
setPubkey: (pubkey: string) => {
set({ pubkey });
},
setTempPrivkey: (privkey: string) => {
set({ tempPrivkey: privkey });
},
setStep: (url: string) => {
set({ step: url });
},
clearStep: () => {
set({ step: null, pubkey: null, tempPrivkey: null });
},
}),
{
name: 'onboarding',
storage: createJSONStorage(() => localStorage),
}
)
);

View File

@@ -1,4 +1,5 @@
import { NDKUser } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKKind, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
import destr from 'destr';
import { LRUCache } from 'lru-cache';
import { NostrEvent } from 'nostr-fetch';
import { nip19 } from 'nostr-tools';
@@ -8,18 +9,22 @@ import {
countTotalNotes,
createChat,
createNote,
getActiveAccount,
getLastLogin,
updateAccount,
} from '@libs/storage';
import { useStronghold } from '@stores/stronghold';
import { nHoursAgo } from '@utils/date';
import { useAccount } from '@utils/hooks/useAccount';
export function useNostr() {
const privkey = useStronghold((state) => state.privkey);
const { ndk, relayUrls, fetcher } = useNDK();
const { account } = useAccount();
async function fetchNetwork(prevFollow?: string[]) {
const account = await getActiveAccount();
const follows = new Set<string>(prevFollow || []);
const lruNetwork = new LRUCache<string, string, void>({ max: 300 });
@@ -27,6 +32,7 @@ export function useNostr() {
// fetch user's follows
if (!prevFollow) {
console.log("fetching user's follow...");
const user = ndk.getUser({ hexpubkey: account.pubkey });
const list = await user.follows();
list.forEach((item: NDKUser) => {
@@ -36,7 +42,7 @@ export function useNostr() {
// fetch network
if (!account.network) {
console.log('fetching network...', follows.size);
console.log("fetching user's network...");
const events = await fetcher.fetchAllEvents(
relayUrls,
{ kinds: [3], authors: [...follows] },
@@ -62,14 +68,15 @@ export function useNostr() {
return [...new Set([...follows, ...network])];
}
const fetchNotes = async (prevFollow?: string[]) => {
async function fetchNotes(prevFollow?: string[]) {
try {
const network = (await fetchNetwork(prevFollow)) as string[];
const network = await fetchNetwork(prevFollow);
const totalNotes = await countTotalNotes();
const lastLogin = await getLastLogin();
if (network.length > 0) {
console.log('fetching notes...');
let since: number;
if (totalNotes === 0 || lastLogin === 0) {
since = nHoursAgo(24);
@@ -96,15 +103,15 @@ export function useNostr() {
}
}
return true;
return { status: 'ok' };
} catch (e) {
console.log('error: ', e);
console.error('failed fetch notes, error: ', e);
return { status: 'failed', message: e };
}
};
}
const fetchChats = async () => {
async function fetchChats() {
try {
const account = await getActiveAccount();
const lastLogin = await getLastLogin();
const incomingMessages = await fetcher.fetchAllEvents(
relayUrls,
@@ -128,11 +135,60 @@ export function useNostr() {
);
}
return true;
return { status: 'ok' };
} catch (e) {
console.log('error: ', e);
console.error('failed fetch incoming messages, error: ', e);
return { status: 'failed', message: e };
}
}
const publish = async ({
content,
kind,
tags,
}: {
content: string;
kind: NDKKind | number;
tags: string[][];
}): Promise<NDKEvent> => {
if (!privkey) throw new Error('Private key not found');
const event = new NDKEvent(ndk);
const signer = new NDKPrivateKeySigner(privkey);
event.content = content;
event.kind = kind;
event.created_at = Math.floor(Date.now() / 1000);
event.pubkey = account.pubkey;
event.tags = tags;
await event.sign(signer);
await event.publish();
return event;
};
return { fetchNotes, fetchChats };
const createZap = async (event: NostrEvent, amount: number, message?: string) => {
// @ts-expect-error, LumeEvent to NostrEvent
event.id = event.event_id;
// @ts-expect-error, LumeEvent to NostrEvent
if (typeof event.content !== 'string') event.content = event.content.original;
if (typeof event.tags === 'string') event.tags = destr(event.tags);
if (!privkey) throw new Error('Private key not found');
if (!ndk.signer) {
const signer = new NDKPrivateKeySigner(privkey);
ndk.signer = signer;
}
const ndkEvent = new NDKEvent(ndk, event);
const res = await ndkEvent.zap(amount, message ?? 'zap from lume');
return res;
};
return { fetchNotes, fetchChats, publish, createZap };
}

View File

@@ -1,60 +0,0 @@
import { NDKEvent, NDKKind, NDKPrivateKeySigner, NostrEvent } from '@nostr-dev-kit/ndk';
import destr from 'destr';
import { useNDK } from '@libs/ndk/provider';
import { useStronghold } from '@stores/stronghold';
import { useAccount } from '@utils/hooks/useAccount';
export function usePublish() {
const { ndk } = useNDK();
const { account } = useAccount();
const privkey = useStronghold((state) => state.privkey);
const publish = async ({
content,
kind,
tags,
}: {
content: string;
kind: NDKKind | number;
tags: string[][];
}): Promise<NDKEvent> => {
if (!privkey) throw new Error('Private key not found');
const event = new NDKEvent(ndk);
const signer = new NDKPrivateKeySigner(privkey);
event.content = content;
event.kind = kind;
event.created_at = Math.floor(Date.now() / 1000);
event.pubkey = account.pubkey;
event.tags = tags;
await event.sign(signer);
await event.publish();
return event;
};
const createZap = async (event: NostrEvent, amount: number, message?: string) => {
// @ts-expect-error, lumeevent to nostrevent
event.id = event.event_id;
// @ts-expect-error, lumeevent to nostrevent
if (typeof event.content !== 'string') event.content = event.content.original;
if (typeof event.tags === 'string') event.tags = destr(event.tags);
if (!privkey) throw new Error('Private key not found');
if (!ndk.signer) {
const signer = new NDKPrivateKeySigner(privkey);
ndk.signer = signer;
}
const ndkEvent = new NDKEvent(ndk, event);
const res = await ndkEvent.zap(amount, message ?? 'test zap from lume');
return res;
};
return { publish, createZap };
}

View File

@@ -5,13 +5,14 @@ import { createNote } from '@libs/storage';
import { nHoursAgo } from '@utils/date';
import { useAccount } from '@utils/hooks/useAccount';
import { usePublish } from '@utils/hooks/usePublish';
import { nip02ToArray } from '@utils/transform';
import { useNostr } from './useNostr';
export function useSocial() {
const queryClient = useQueryClient();
const { publish } = usePublish();
const { publish } = useNostr();
const { fetcher, relayUrls } = useNDK();
const { account } = useAccount();
const { status, data: userFollows } = useQuery(

View File

@@ -3,10 +3,10 @@ import { open } from '@tauri-apps/plugin-dialog';
import { VoidApi } from '@void-cat/api';
import { createBlobFromFile } from '@utils/createBlobFromFile';
import { usePublish } from '@utils/hooks/usePublish';
import { useNostr } from '@utils/hooks/useNostr';
export function useImageUploader() {
const { publish } = usePublish();
const { publish } = useNostr();
const upload = async (file: null | string, nip94?: boolean) => {
const voidcat = new VoidApi('https://void.cat');