improve startup time

This commit is contained in:
Ren Amamiya 2023-09-27 14:53:01 +07:00
parent b339e842ca
commit 2b50fc438f
13 changed files with 244 additions and 142 deletions

View File

@ -16,7 +16,7 @@ export function ImportStep3Screen() {
const setStep = useOnboarding((state) => state.setStep);
const { db } = useStorage();
const { fetchUserData, prefetchEvents } = useNostr();
const { fetchUserData } = useNostr();
const [loading, setLoading] = useState(false);
@ -27,16 +27,14 @@ export function ImportStep3Screen() {
// prefetch data
const user = await fetchUserData();
const data = await prefetchEvents();
// create default widget
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
// redirect to next step
if (user.status === 'ok' && data.status === 'ok') {
if (user.status === 'ok') {
navigate('/auth/onboarding/step-2', { replace: true });
} else {
console.log('error: ', data.message);
setLoading(false);
}
} catch (e) {
@ -82,8 +80,8 @@ export function ImportStep3Screen() {
)}
</button>
<span className="text-center text-sm text-white/50">
By clicking &apos;Continue&apos;, Lume will download your old relay list and
all events from the last 24 hours. It may take a bit
By clicking &apos;Continue&apos;, Lume will sync your old relay list and
metadata. It may take a bit
</span>
</div>
</div>

View File

@ -16,7 +16,7 @@ export function OnboardStep1Screen() {
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const { publish, fetchUserData, prefetchEvents } = useNostr();
const { publish, fetchUserData } = useNostr();
const { db } = useStorage();
const { status, data } = useQuery(['trending-profiles-widget'], async () => {
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
@ -46,14 +46,12 @@ export function OnboardStep1Screen() {
// prefetch data
const user = await fetchUserData(follows);
const data = await prefetchEvents();
// redirect to next step
if (event && user.status === 'ok' && data.status === 'ok') {
if (event && user.status === 'ok') {
navigate('/auth/onboarding/step-2', { replace: true });
} else {
setLoading(false);
console.log('error: ', data.message);
}
} catch (e) {
setLoading(false);
@ -70,7 +68,7 @@ export function OnboardStep1Screen() {
<div className="flex h-full w-full flex-col justify-center">
<div className="mx-auto mb-4 w-full max-w-md border-b border-white/10 pb-4">
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
{loading ? 'Prefetching data...' : 'Enrich your network'}
{loading ? 'Loading...' : 'Enrich your network'}
</h1>
<p className="text-white/70">
Choose the account you want to follow. These accounts are trending in the last
@ -127,19 +125,12 @@ export function OnboardStep1Screen() {
</>
)}
</button>
{!loading ? (
<Link
to="/auth/onboarding/step-2"
className="inline-flex h-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/30 focus:outline-none"
>
Skip, you can add later
</Link>
) : (
<span className="text-center text-sm text-white/50">
By clicking &apos;Continue&apos;, Lume will download all events related to
your follows from the last 24 hours. It may take a bit
</span>
)}
<Link
to="/auth/onboarding/step-2"
className="inline-flex h-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/30 focus:outline-none"
>
Skip, you can add later
</Link>
</div>
</div>
</div>

View File

@ -65,9 +65,9 @@ export function SpaceScreen() {
case WidgetKinds.nostrBand.trendingNotes:
return <TrendingNotesWidget key={widget.id} params={widget} />;
case WidgetKinds.tmp.xfeed:
return <XhashtagWidget key={widget.id} params={widget} />;
case WidgetKinds.tmp.xhashtag:
return <XfeedsWidget key={widget.id} params={widget} />;
case WidgetKinds.tmp.xhashtag:
return <XhashtagWidget key={widget.id} params={widget} />;
case WidgetKinds.tmp.list:
return <WidgetList key={widget.id} params={widget} />;
case WidgetKinds.other.learnNostr:

View File

@ -12,7 +12,7 @@ import { useNostr } from '@utils/hooks/useNostr';
export function SplashScreen() {
const { db } = useStorage();
const { ndk } = useNDK();
const { fetchUserData, prefetchEvents } = useNostr();
const { fetchUserData } = useNostr();
const [isLoading, setIsLoading] = useState<boolean>(true);
@ -20,27 +20,8 @@ export function SplashScreen() {
await invoke('close_splashscreen');
};
const prefetch = async () => {
try {
const [user, events] = await Promise.all([fetchUserData(), prefetchEvents()]);
if (user.status === 'ok' && events.status === 'ok') {
// update last login = current time
await db.updateLastLogin();
// close splash screen and open main app screen
await invoke('close_splashscreen');
}
} catch (e) {
setIsLoading(false);
await message(e, {
title: 'An unexpected error has occurred',
type: 'error',
});
}
};
useEffect(() => {
async function initial() {
async function syncUserData() {
if (!db.account) {
await invoke('close_splashscreen');
} else {
@ -50,31 +31,47 @@ export function SplashScreen() {
if (step) {
await invoke('close_splashscreen');
} else {
console.log('prefetching...');
prefetch();
try {
const userData = await fetchUserData();
if (userData.status === 'ok') {
// update last login = current time
await db.updateLastLogin();
// close splash screen and open main app screen
await invoke('close_splashscreen');
}
} catch (e) {
setIsLoading(false);
await message(e, {
title: 'An unexpected error has occurred',
type: 'error',
});
}
}
}
}
if (ndk) {
initial();
syncUserData();
}
}, [ndk, db.account]);
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 min-h-0 w-full flex-1 items-center justify-center px-8">
<div className="flex flex-col items-center justify-center gap-4">
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
{isLoading ? (
<div className="flex flex-col gap-1 text-center">
<div className="flex flex-col gap-2 text-center">
<h3 className="text-lg font-semibold leading-none text-white">
{!ndk ? 'Connecting to relay...' : 'Fetching events from the last login.'}
{!ndk ? 'Connecting to relay...' : 'Syncing user data...'}
</h3>
<p className="text-sm text-white/50">
This may take a few seconds, please don&apos;t close app.
</p>
{ndk ? (
<p className="text-sm text-white/50">
Ensure all your data is sync across all Nostr clients, it may take a few
seconds, please don&apos;t close app.
</p>
) : null}
</div>
) : (
<div className="mt-2 flex flex-col gap-1 text-center">

View File

@ -21,49 +21,42 @@ export const NDKInstance = () => {
);
// TODO: fully support NIP-11
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function getExplicitRelays() {
try {
// get relays
const relays = await db.getExplicitRelayUrls();
const requests = relays.map((relay) => {
const onlineRelays = new Set(relays);
for (const relay of relays) {
const url = new URL(relay);
return fetch(`https://${url.hostname + url.pathname}`, {
method: 'GET',
timeout: 10,
headers: {
Accept: 'application/nostr+json',
},
});
});
try {
const res = await fetch(`https://${url.hostname}`, {
method: 'GET',
timeout: { secs: 5, nanos: 0 },
headers: {
Accept: 'application/nostr+json',
},
});
const responses = await Promise.all(requests);
const successes = responses.filter((res) => res.ok);
const verifiedRelays: string[] = successes.map((res) => {
const url = new URL(res.url);
// @ts-expect-error, not have type yet
if (res.data?.limitation?.payment_required) {
if (url.protocol === 'http:')
return `ws://${url.hostname + url.pathname + db.account.npub}`;
if (url.protocol === 'https:')
return `wss://${url.hostname + url.pathname + db.account.npub}`;
if (!res.ok) {
console.info(`${relay} is not working, skipping...`);
onlineRelays.delete(relay);
}
} catch {
console.warn(`${relay} is not working, skipping...`);
onlineRelays.delete(relay);
}
}
if (url.protocol === 'http:') return `ws://${url.hostname + url.pathname}`;
if (url.protocol === 'https:') return `wss://${url.hostname + url.pathname}`;
});
// return all validated relays
return verifiedRelays;
// return all online relays
return [...onlineRelays];
} catch (e) {
console.error(e);
}
}
async function initNDK() {
const explicitRelayUrls = await db.getExplicitRelayUrls();
const explicitRelayUrls = await getExplicitRelays();
const instance = new NDK({
explicitRelayUrls,
cacheAdapter,

View File

@ -1,5 +1,6 @@
import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk';
import { BaseDirectory, removeFile } from '@tauri-apps/api/fs';
import { Platform } from '@tauri-apps/api/os';
import Database from 'tauri-plugin-sql-api';
import { Stronghold } from 'tauri-plugin-stronghold-api';
@ -11,12 +12,14 @@ import { Account, DBEvent, Relays, Widget } from '@utils/types';
export class LumeStorage {
public db: Database;
public secureDB: Stronghold;
public account: Account | null = null;
public account: Account | null;
public platform: Platform | null;
constructor(sqlite: Database, stronghold?: Stronghold) {
constructor(sqlite: Database, platform?: Platform, stronghold?: Stronghold) {
this.db = sqlite;
this.secureDB = stronghold ?? undefined;
this.account = null;
this.platform = platform ?? undefined;
}
private async getSecureClient(key?: string) {

View File

@ -1,4 +1,5 @@
import { message } from '@tauri-apps/api/dialog';
import { platform } from '@tauri-apps/api/os';
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
@ -18,7 +19,8 @@ const StorageProvider = ({ children }: PropsWithChildren<object>) => {
const initLumeStorage = async () => {
try {
const sqlite = await Database.load('sqlite:lume.db');
const lumeStorage = new LumeStorage(sqlite);
const platformName = await platform();
const lumeStorage = new LumeStorage(sqlite, platformName);
if (!lumeStorage.account) await lumeStorage.getActiveAccount();
setDB(lumeStorage);

View File

@ -0,0 +1,61 @@
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useStorage } from '@libs/storage/provider';
import { useNostr } from '@utils/hooks/useNostr';
export function EmptyList() {
const { db } = useStorage();
const { getAllEventsSinceLastLogin } = useNostr();
const queryClient = useQueryClient();
const [progress, setProgress] = useState(0);
useEffect(() => {
async function getEvents() {
const events = await getAllEventsSinceLastLogin();
const promises = await Promise.all(
events.data.map(async (event) => await db.createEvent(event))
);
if (promises) {
setProgress(100);
// invalidate queries
queryClient.invalidateQueries(['local-network-widget']);
}
}
// only start download if progress === 0
if (progress === 0) getEvents();
// auto increase progress after 2 secs
setInterval(() => setProgress((prev) => (prev += 8)), 2000);
}, []);
return (
<div className="px-3">
<div className="h-max w-full rounded-lg border-t border-white/10 bg-white/20 p-3">
<div className="flex flex-col items-center gap-5">
<div>
<span className="text-4xl">👋</span>
<h3 className="mt-2 font-semibold leading-tight">
Hello, this is the first time you&apos;re using Lume
</h3>
<p className="text-sm text-white/70">
Lume is downloading all events since the last 24 hours. It will auto refresh
when it done, please be patient
</p>
</div>
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-white/20">
<div
className="flex flex-col justify-center overflow-hidden bg-fuchsia-500 transition-all duration-1000 ease-smooth"
role="progressbar"
style={{ width: `${progress}%` }}
/>
</div>
</div>
</div>
</div>
);
}

View File

@ -14,3 +14,5 @@ export * from './nostrBand/trendingAccounts';
export * from './tmp/feeds';
export * from './tmp/hashtag';
export * from './other/learnNostr';
export * from './emptyList';
export * from './loadLatestEvents';

View File

@ -0,0 +1,61 @@
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useStorage } from '@libs/storage/provider';
import { useStronghold } from '@stores/stronghold';
import { useNostr } from '@utils/hooks/useNostr';
export function LoadLatestEvents() {
const { db } = useStorage();
const { getAllEventsSinceLastLogin } = useNostr();
const setIsFetched = useStronghold((state) => state.setIsFetched);
const queryClient = useQueryClient();
const [progress, setProgress] = useState(0);
useEffect(() => {
async function getEvents() {
const events = await getAllEventsSinceLastLogin();
const promises = await Promise.all(
events.data.map(async (event) => await db.createEvent(event))
);
if (promises) {
setProgress(100);
setIsFetched();
// invalidate queries
queryClient.invalidateQueries(['local-network-widget']);
}
}
// only start download if progress === 0
if (progress === 0) getEvents();
// auto increase progress after 2 secs
setInterval(() => setProgress((prev) => (prev += 8)), 2000);
}, []);
return (
<div className="mb-3 px-3">
<div className="h-max w-full rounded-lg border-t border-white/10 bg-white/20 p-3">
<div className="flex flex-col items-center gap-3">
<div className="text-center">
<h3 className="font-semibold leading-tight">
Downloading all events from your last login...
</h3>
</div>
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-white/20">
<div
className="flex flex-col justify-center overflow-hidden bg-fuchsia-500 transition-all duration-1000 ease-smooth"
role="progressbar"
style={{ width: `${progress}%` }}
/>
</div>
</div>
</div>
</div>
);
}

View File

@ -16,7 +16,9 @@ import {
} from '@shared/notes';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { EmptyList, LoadLatestEvents, WidgetWrapper } from '@shared/widgets';
import { useStronghold } from '@stores/stronghold';
import { useNostr } from '@utils/hooks/useNostr';
import { toRawEvent } from '@utils/rawEvent';
@ -34,6 +36,7 @@ export function LocalNetworkWidget() {
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
const isFetched = useStronghold((state) => state.isFetched);
const dbEvents = useMemo(
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
@ -83,7 +86,7 @@ export function LocalNetworkWidget() {
// subscribe for new event
// sub will be managed by lru-cache
useEffect(() => {
if (db.account && db.account.network) {
if (db.account && db.account.network && dbEvents.length > 0) {
const filter: NDKFilter = {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: db.account.network,
@ -95,11 +98,11 @@ export function LocalNetworkWidget() {
await db.createEvent(rawEvent);
});
}
}, []);
}, [data]);
return (
<WidgetWrapper>
<TitleBar title="👋 Network" />
<TitleBar title="Network" />
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
@ -108,21 +111,10 @@ export function LocalNetworkWidget() {
</div>
</div>
) : dbEvents.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
<EmptyList />
) : (
<VList className="scrollbar-hide h-full">
{!isFetched ? <LoadLatestEvents /> : null}
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (

View File

@ -4,9 +4,11 @@ import { createJSONStorage, persist } from 'zustand/middleware';
interface StrongholdState {
privkey: null | string;
walletConnectURL: null | string;
isFetched: null | boolean;
setPrivkey: (privkey: string) => void;
setWalletConnectURL: (uri: string) => void;
clearPrivkey: () => void;
setIsFetched: () => void;
}
export const useStronghold = create<StrongholdState>()(
@ -14,6 +16,7 @@ export const useStronghold = create<StrongholdState>()(
(set) => ({
privkey: null,
walletConnectURL: null,
isFetched: false,
setPrivkey: (privkey: string) => {
set({ privkey: privkey });
},
@ -23,6 +26,9 @@ export const useStronghold = create<StrongholdState>()(
clearPrivkey: () => {
set({ privkey: null });
},
setIsFetched: () => {
set({ isFetched: true });
},
}),
{
name: 'stronghold',

View File

@ -54,7 +54,7 @@ export function useNostr() {
});
subManager.set(JSON.stringify(filter), subEvent);
console.log(subManager.keys());
console.log('current active sub: ', subManager.size);
};
const fetchUserData = async (preFollows?: string[]) => {
@ -144,41 +144,6 @@ export function useNostr() {
publish({ content: '', kind: NDKKind.Contacts, tags: tags });
};
const prefetchEvents = async () => {
try {
const dbEventsEmpty = await db.isEventsEmpty();
let since: number;
if (dbEventsEmpty || db.account.last_login_at === 0) {
since = db.account.network.length > 400 ? nHoursAgo(12) : nHoursAgo(24);
} else {
since = db.account.last_login_at;
}
console.log("prefetching events with user's network: ", db.account.network.length);
console.log('prefetching events since: ', since);
const events = (await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
authors: db.account.network,
},
{ since: since }
)) as unknown as NDKEvent[];
// save all events to database
const promises = await Promise.all(
events.map(async (event) => await db.createEvent(event))
);
if (promises) return { status: 'ok', message: 'prefetch completed' };
} catch (e) {
console.error('prefetch events failed, error: ', e);
return { status: 'failed', message: e };
}
};
const fetchActivities = async () => {
try {
const events = await fetcher.fetchAllEvents(
@ -293,6 +258,37 @@ export function useNostr() {
return events;
};
const getAllEventsSinceLastLogin = async (customSince?: number) => {
try {
let since: number;
const dbEventsEmpty = await db.isEventsEmpty();
if (!customSince) {
if (dbEventsEmpty || db.account.last_login_at === 0) {
since = db.account.network.length > 400 ? nHoursAgo(12) : nHoursAgo(24);
} else {
since = db.account.last_login_at;
}
} else {
since = customSince;
}
const events = (await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
authors: db.account.network,
},
{ since: since }
)) as unknown as NDKEvent[];
return { status: 'ok', message: 'fetch completed', data: events };
} catch (e) {
console.error('prefetch events failed, error: ', e);
return { status: 'failed', message: e };
}
};
const getContactsByPubkey = async (pubkey: string) => {
const user = ndk.getUser({ hexpubkey: pubkey });
const follows = [...(await user.follows())].map((user) => user.hexpubkey);
@ -444,7 +440,7 @@ export function useNostr() {
fetchUserData,
addContact,
removeContact,
prefetchEvents,
getAllEventsSinceLastLogin,
fetchActivities,
fetchNIP04Chats,
fetchNIP04Messages,