diff --git a/src/app/auth/import/step-3.tsx b/src/app/auth/import/step-3.tsx index 66f9f6dc..2c51dbaa 100644 --- a/src/app/auth/import/step-3.tsx +++ b/src/app/auth/import/step-3.tsx @@ -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() { )} - By clicking 'Continue', Lume will download your old relay list and - all events from the last 24 hours. It may take a bit + By clicking 'Continue', Lume will sync your old relay list and + metadata. It may take a bit diff --git a/src/app/auth/onboarding/step-1.tsx b/src/app/auth/onboarding/step-1.tsx index 1f9d2dd0..08b4bc06 100644 --- a/src/app/auth/onboarding/step-1.tsx +++ b/src/app/auth/onboarding/step-1.tsx @@ -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() {

- {loading ? 'Prefetching data...' : 'Enrich your network'} + {loading ? 'Loading...' : 'Enrich your network'}

Choose the account you want to follow. These accounts are trending in the last @@ -127,19 +125,12 @@ export function OnboardStep1Screen() { )} - {!loading ? ( - - Skip, you can add later - - ) : ( - - By clicking 'Continue', Lume will download all events related to - your follows from the last 24 hours. It may take a bit - - )} + + Skip, you can add later +

diff --git a/src/app/space/index.tsx b/src/app/space/index.tsx index d77bcc39..9a5a6533 100644 --- a/src/app/space/index.tsx +++ b/src/app/space/index.tsx @@ -65,9 +65,9 @@ export function SpaceScreen() { case WidgetKinds.nostrBand.trendingNotes: return ; case WidgetKinds.tmp.xfeed: - return ; - case WidgetKinds.tmp.xhashtag: return ; + case WidgetKinds.tmp.xhashtag: + return ; case WidgetKinds.tmp.list: return ; case WidgetKinds.other.learnNostr: diff --git a/src/app/splash.tsx b/src/app/splash.tsx index 30019d04..d984041a 100644 --- a/src/app/splash.tsx +++ b/src/app/splash.tsx @@ -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(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 (
-
+
{isLoading ? ( -
+

- {!ndk ? 'Connecting to relay...' : 'Fetching events from the last login.'} + {!ndk ? 'Connecting to relay...' : 'Syncing user data...'}

-

- This may take a few seconds, please don't close app. -

+ {ndk ? ( +

+ Ensure all your data is sync across all Nostr clients, it may take a few + seconds, please don't close app. +

+ ) : null}
) : (
diff --git a/src/libs/ndk/instance.ts b/src/libs/ndk/instance.ts index 01709c76..4bfbaef9 100644 --- a/src/libs/ndk/instance.ts +++ b/src/libs/ndk/instance.ts @@ -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, diff --git a/src/libs/storage/instance.ts b/src/libs/storage/instance.ts index 7f622da6..b89ac007 100644 --- a/src/libs/storage/instance.ts +++ b/src/libs/storage/instance.ts @@ -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) { diff --git a/src/libs/storage/provider.tsx b/src/libs/storage/provider.tsx index c9bb39a8..718a1a9f 100644 --- a/src/libs/storage/provider.tsx +++ b/src/libs/storage/provider.tsx @@ -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) => { 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); diff --git a/src/shared/widgets/emptyList.tsx b/src/shared/widgets/emptyList.tsx new file mode 100644 index 00000000..bce8ab1a --- /dev/null +++ b/src/shared/widgets/emptyList.tsx @@ -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 ( +
+
+
+
+ 👋 +

+ Hello, this is the first time you're using Lume +

+

+ Lume is downloading all events since the last 24 hours. It will auto refresh + when it done, please be patient +

+
+
+
+
+
+
+
+ ); +} diff --git a/src/shared/widgets/index.ts b/src/shared/widgets/index.ts index fd0dcdbf..31474425 100644 --- a/src/shared/widgets/index.ts +++ b/src/shared/widgets/index.ts @@ -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'; diff --git a/src/shared/widgets/loadLatestEvents.tsx b/src/shared/widgets/loadLatestEvents.tsx new file mode 100644 index 00000000..d98e9f73 --- /dev/null +++ b/src/shared/widgets/loadLatestEvents.tsx @@ -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 ( +
+
+
+
+

+ Downloading all events from your last login... +

+
+
+
+
+
+
+
+ ); +} diff --git a/src/shared/widgets/local/network.tsx b/src/shared/widgets/local/network.tsx index e0938d5f..a1c9ebac 100644 --- a/src/shared/widgets/local/network.tsx +++ b/src/shared/widgets/local/network.tsx @@ -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 ( - +
{status === 'loading' ? (
@@ -108,21 +111,10 @@ export function LocalNetworkWidget() {
) : dbEvents.length === 0 ? ( -
-
- empty feeds -
-

- Your newsfeed is empty -

-

- Connect more people to explore more content -

-
-
-
+ ) : ( + {!isFetched ? : null} {dbEvents.map((item) => renderItem(item))}
{dbEvents.length > 0 ? ( diff --git a/src/stores/stronghold.ts b/src/stores/stronghold.ts index b319c0ec..c1454051 100644 --- a/src/stores/stronghold.ts +++ b/src/stores/stronghold.ts @@ -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()( @@ -14,6 +16,7 @@ export const useStronghold = create()( (set) => ({ privkey: null, walletConnectURL: null, + isFetched: false, setPrivkey: (privkey: string) => { set({ privkey: privkey }); }, @@ -23,6 +26,9 @@ export const useStronghold = create()( clearPrivkey: () => { set({ privkey: null }); }, + setIsFetched: () => { + set({ isFetched: true }); + }, }), { name: 'stronghold', diff --git a/src/utils/hooks/useNostr.ts b/src/utils/hooks/useNostr.ts index 98b54626..75f517fb 100644 --- a/src/utils/hooks/useNostr.ts +++ b/src/utils/hooks/useNostr.ts @@ -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,