From e7738fb128f17c0ea1d04fb6113551b0ada20e5b Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 30 Oct 2023 16:29:49 +0700 Subject: [PATCH] refactor newsfeed widget --- package.json | 2 + pnpm-lock.yaml | 36 ++++ src/app/space/index.tsx | 4 +- src/main.jsx | 20 +- src/shared/notes/kinds/repost.tsx | 30 +-- src/shared/notes/skeleton.tsx | 14 +- src/shared/notes/wrapper.tsx | 26 ++- src/shared/widgets/eventLoader.tsx | 78 -------- src/shared/widgets/index.ts | 3 +- src/shared/widgets/local/follows.tsx | 2 - src/shared/widgets/local/network.tsx | 184 ----------------- src/shared/widgets/newsfeed.tsx | 187 ++++++++++++++++++ .../widgets/{local => }/notification.tsx | 7 +- src/stores/widgets.ts | 2 + vite.config.ts | 8 +- 15 files changed, 277 insertions(+), 326 deletions(-) delete mode 100644 src/shared/widgets/eventLoader.tsx delete mode 100644 src/shared/widgets/local/network.tsx create mode 100644 src/shared/widgets/newsfeed.tsx rename src/shared/widgets/{local => }/notification.tsx (93%) diff --git a/package.json b/package.json index 10d8edcb..0b7e1437 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,9 @@ "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-toolbar": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", + "@tanstack/query-sync-storage-persister": "^5.4.3", "@tanstack/react-query": "^5.0.5", + "@tanstack/react-query-persist-client": "^5.4.3", "@tauri-apps/api": "^2.0.0-alpha.9", "@tauri-apps/cli": "^2.0.0-alpha.16", "@tauri-apps/plugin-clipboard-manager": "^2.0.0-alpha.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11781526..b3f5fdfd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,9 +47,15 @@ dependencies: '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) + '@tanstack/query-sync-storage-persister': + specifier: ^5.4.3 + version: 5.4.3 '@tanstack/react-query': specifier: ^5.0.5 version: 5.0.5(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-query-persist-client': + specifier: ^5.4.3 + version: 5.4.3(@tanstack/react-query@5.0.5)(react-dom@18.2.0)(react@18.2.0) '@tauri-apps/api': specifier: ^2.0.0-alpha.9 version: 2.0.0-alpha.9 @@ -2096,6 +2102,36 @@ packages: resolution: {integrity: sha512-MThCETMkHDHTnFZHp71L+SqTtD5d6XHftFCVR1xRJdWM3qGrlQ2VCXaj0SKVcyJej2e1Opa2c7iknu1llxCDNQ==} dev: false + /@tanstack/query-core@5.4.3: + resolution: {integrity: sha512-fnI9ORjcuLGm1sNrKatKIosRQUpuqcD4SV7RqRSVmj8JSicX2aoMyKryHEBpVQvf6N4PaBVgBxQomjsbsGPssQ==} + dev: false + + /@tanstack/query-persist-client-core@5.4.3: + resolution: {integrity: sha512-0MZazQMVXmmVyf/ce2ug0CoSkT02VA4ZhkT3F1/tIINxGuH2KlhKWQc9puqJzTazUpXfRdBK9+lMPqpkA16FEQ==} + dependencies: + '@tanstack/query-core': 5.4.3 + dev: false + + /@tanstack/query-sync-storage-persister@5.4.3: + resolution: {integrity: sha512-53e2O8lLaeBZ26myG6zQt5Ix16XmkcqJrsrSP2ZZzP5Ii6XwBq061djaEMTNbWYenYpmDqQIxAye/J6zQZ0QiA==} + dependencies: + '@tanstack/query-core': 5.4.3 + '@tanstack/query-persist-client-core': 5.4.3 + dev: false + + /@tanstack/react-query-persist-client@5.4.3(@tanstack/react-query@5.0.5)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-gpusG6IG6rnmdRT3onSjmRVG60K3BlsdUQifBVeLdi4uen1rvRAiB5a7jr4hFVMItzS9C4jBxJMWt/DmZpy6Ow==} + peerDependencies: + '@tanstack/react-query': ^5.4.3 + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@tanstack/query-persist-client-core': 5.4.3 + '@tanstack/react-query': 5.0.5(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@tanstack/react-query@5.0.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ZG0Q4HZ0iuI8mWiZ2/MdVYPHbrmAVhMn7+gLOkxJh6zLIgCL4luSZlohzN5Xt4MjxfxxWioO1nemwpudaTsmQg==} peerDependencies: diff --git a/src/app/space/index.tsx b/src/app/space/index.tsx index 97c1557f..5ffee4d6 100644 --- a/src/app/space/index.tsx +++ b/src/app/space/index.tsx @@ -16,10 +16,10 @@ import { LocalFeedsWidget, LocalFilesWidget, LocalFollowsWidget, - LocalNetworkWidget, LocalNotificationWidget, LocalThreadWidget, LocalUserWidget, + NewsfeedWidget, TrendingAccountsWidget, TrendingNotesWidget, XfeedsWidget, @@ -44,7 +44,7 @@ export function SpaceScreen() { if (!widget) return; switch (widget.kind) { case WidgetKinds.local.network: - return ; + return ; case WidgetKinds.local.follows: return ; case WidgetKinds.local.feeds: diff --git a/src/main.jsx b/src/main.jsx index 1e152d8e..88fe7483 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,4 +1,6 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'; +import { QueryClient } from '@tanstack/react-query'; +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; import { createRoot } from 'react-dom/client'; import { Toaster } from 'sonner'; @@ -7,18 +9,28 @@ import { StorageProvider } from '@libs/storage/provider'; import App from './app'; -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 1000 * 60 * 60 * 24, // 24 hours + }, + }, +}); + +const persister = createSyncStoragePersister({ + storage: window.localStorage, +}); const container = document.getElementById('root'); const root = createRoot(container); root.render( - + - + ); diff --git a/src/shared/notes/kinds/repost.tsx b/src/shared/notes/kinds/repost.tsx index fd095bfb..45645ef7 100644 --- a/src/shared/notes/kinds/repost.tsx +++ b/src/shared/notes/kinds/repost.tsx @@ -2,7 +2,6 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { useQuery } from '@tanstack/react-query'; import { nip19 } from 'nostr-tools'; import { memo, useCallback } from 'react'; -import { twMerge } from 'tailwind-merge'; import { useNDK } from '@libs/ndk/provider'; @@ -17,13 +16,7 @@ import { } from '@shared/notes'; import { User } from '@shared/user'; -export function Repost({ - event, - lighter = false, -}: { - event: NDKEvent; - lighter?: boolean; -}) { +export function Repost({ event }: { event: NDKEvent }) { const embedEvent: null | NDKEvent = event.content.length > 0 ? JSON.parse(event.content) : null; @@ -59,12 +52,7 @@ export function Repost({ if (embedEvent) { return (
-
+
-
+
@@ -133,12 +116,7 @@ export function Repost({ return (
-
+
diff --git a/src/shared/notes/skeleton.tsx b/src/shared/notes/skeleton.tsx index a0385c2b..6fdde6ce 100644 --- a/src/shared/notes/skeleton.tsx +++ b/src/shared/notes/skeleton.tsx @@ -2,17 +2,17 @@ export function NoteSkeleton() { return (
-
+
-
+
-
+
-
-
-
-
+
+
+
+
diff --git a/src/shared/notes/wrapper.tsx b/src/shared/notes/wrapper.tsx index 27d6d5b5..464e4eb3 100644 --- a/src/shared/notes/wrapper.tsx +++ b/src/shared/notes/wrapper.tsx @@ -1,6 +1,5 @@ import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { ReactElement, cloneElement } from 'react'; -import { twMerge } from 'tailwind-merge'; +import { ReactElement, cloneElement, useMemo } from 'react'; import { ChildNote, NoteActions } from '@shared/notes'; import { User } from '@shared/user'; @@ -8,25 +7,22 @@ import { User } from '@shared/user'; export function NoteWrapper({ event, children, - root, - reply, - lighter = false, }: { event: NDKEvent; children: ReactElement; - repost?: boolean; - root?: string; - reply?: string; - lighter?: boolean; }) { + const root = useMemo(() => { + if (event.tags?.[0]?.[0] === 'e' && !event.tags?.[0]?.[3]) { + return event.tags[0][1]; + } + return event.tags.find((el) => el[3] === 'root')?.[1]; + }, [event]); + + const reply = useMemo(() => event.tags.find((el) => el[3] === 'reply')?.[1], []); + return (
-
+
{root && }
{reply && }
diff --git a/src/shared/widgets/eventLoader.tsx b/src/shared/widgets/eventLoader.tsx deleted file mode 100644 index 3917af3a..00000000 --- a/src/shared/widgets/eventLoader.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { useEffect, useState } from 'react'; - -import { useStorage } from '@libs/storage/provider'; - -import { useWidgets } from '@stores/widgets'; - -import { useNostr } from '@utils/hooks/useNostr'; - -export function EventLoader({ firstTime }: { firstTime: boolean }) { - const { db } = useStorage(); - const { getAllEventsSinceLastLogin } = useNostr(); - - const [progress, setProgress] = useState(0); - - const queryClient = useQueryClient(); - const setIsFetched = useWidgets((state) => state.setIsFetched); - - useEffect(() => { - async function getEvents() { - const events = await getAllEventsSinceLastLogin(); - console.log('total new events has found: ', events.length); - - if (events) { - setProgress(100); - setIsFetched(); - - // invalidate queries - await queryClient.invalidateQueries({ - queryKey: ['local-network-widget'], - }); - - // update last login time, use for next fetch - await db.updateLastLogin(); - } - } - - // only start download if progress === 0 - if (progress === 0) getEvents(); - - // auto increase progress after 2 secs - setInterval(() => setProgress((prev) => (prev += 5)), 2000); - }, []); - - return ( -
-
-
- {firstTime ? ( -
- 👋 -

- 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 -

-
- ) : ( -
-

- Downloading all events while you're away... -

-
- )} -
-
-
-
-
-
- ); -} diff --git a/src/shared/widgets/index.ts b/src/shared/widgets/index.ts index 0402c585..61bb53fb 100644 --- a/src/shared/widgets/index.ts +++ b/src/shared/widgets/index.ts @@ -6,7 +6,6 @@ export * from './local/thread'; export * from './local/files'; export * from './local/articles'; export * from './local/follows'; -export * from './local/notification'; export * from './global/articles'; export * from './global/files'; export * from './global/hashtag'; @@ -16,3 +15,5 @@ export * from './tmp/feeds'; export * from './tmp/hashtag'; export * from './other/learnNostr'; export * from './eventLoader'; +export * from './newsfeed'; +export * from './notification'; diff --git a/src/shared/widgets/local/follows.tsx b/src/shared/widgets/local/follows.tsx index f69833a7..cb2836fb 100644 --- a/src/shared/widgets/local/follows.tsx +++ b/src/shared/widgets/local/follows.tsx @@ -46,8 +46,6 @@ export function LocalFollowsWidget({ params }: { params: Widget }) { diff --git a/src/shared/widgets/local/network.tsx b/src/shared/widgets/local/network.tsx deleted file mode 100644 index 7f6c8138..00000000 --- a/src/shared/widgets/local/network.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk'; -import { useInfiniteQuery } from '@tanstack/react-query'; -import { useCallback, useEffect, useMemo } from 'react'; -import { VList } from 'virtua'; - -import { useStorage } from '@libs/storage/provider'; - -import { ArrowRightCircleIcon, ArrowRightIcon, LoaderIcon } from '@shared/icons'; -import { - MemoizedArticleNote, - MemoizedFileNote, - MemoizedRepost, - MemoizedTextNote, - NoteWrapper, - UnknownNote, -} from '@shared/notes'; -import { NoteSkeleton } from '@shared/notes/skeleton'; -import { TitleBar } from '@shared/titleBar'; -import { EventLoader, WidgetWrapper } from '@shared/widgets'; - -import { WidgetKinds, useWidgets } from '@stores/widgets'; - -import { useNostr } from '@utils/hooks/useNostr'; -import { DBEvent } from '@utils/types'; - -export function LocalNetworkWidget() { - const { sub } = useNostr(); - const { db } = useStorage(); - const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = - useInfiniteQuery({ - queryKey: ['local-network-widget'], - initialPageParam: 0, - queryFn: async ({ pageParam = 0 }) => { - return await db.getAllEvents(20, pageParam); - }, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }); - - const setWidget = useWidgets((state) => state.setWidget); - const isFetched = useWidgets((state) => state.isFetched); - const dbEvents = useMemo( - () => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []), - [data] - ); - - // render event match event kind - const renderItem = useCallback( - (dbEvent: DBEvent) => { - const event: NDKEvent = JSON.parse(dbEvent.event as string); - switch (event.kind) { - case NDKKind.Text: - return ( - - - - ); - case NDKKind.Repost: - return ; - case 1063: - return ( - - - - ); - case NDKKind.Article: - return ( - - - - ); - default: - return ( - - - - ); - } - }, - [dbEvents] - ); - - const openTrendingWidgets = async () => { - setWidget(db, { - kind: WidgetKinds.nostrBand.trendingAccounts, - title: 'Trending Accounts', - content: '', - }); - }; - - // subscribe for new event - // sub will be managed by lru-cache - useEffect(() => { - if (db.account && db.account.circles.length > 0 && dbEvents.length > 0) { - const filter: NDKFilter = { - kinds: [NDKKind.Text, NDKKind.Repost], - authors: db.account.circles, - since: Math.floor(Date.now() / 1000), - }; - - sub(filter, async (event) => { - await db.createEvent(event); - }); - } - }, [data]); - - if (db.account.circles.length < 1) { - return ( - -
-
-

👋

-

You have not follow anyone yet

-
- If you are new to Nostr, you can click button below to open trending users - and start follow some of theme -
- -
-
-
- ); - } - - return ( - - -
- {status === 'pending' ? ( -
-
- -
-
- ) : dbEvents.length === 0 ? ( - - ) : ( - - {!isFetched ? : null} - {dbEvents.map((item) => renderItem(item))} -
- {dbEvents.length > 0 ? ( - - ) : null} -
-
- - )} -
- - ); -} diff --git a/src/shared/widgets/newsfeed.tsx b/src/shared/widgets/newsfeed.tsx new file mode 100644 index 00000000..fa6a7f74 --- /dev/null +++ b/src/shared/widgets/newsfeed.tsx @@ -0,0 +1,187 @@ +import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useCallback, useEffect } from 'react'; +import { VList } from 'virtua'; + +import { useNDK } from '@libs/ndk/provider'; +import { useStorage } from '@libs/storage/provider'; + +import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; +import { + MemoizedArticleNote, + MemoizedFileNote, + MemoizedRepost, + MemoizedTextNote, + NoteSkeleton, + NoteWrapper, + UnknownNote, +} from '@shared/notes'; +import { TitleBar } from '@shared/titleBar'; +import { WidgetWrapper } from '@shared/widgets'; + +import { nHoursAgo } from '@utils/date'; +import { useNostr } from '@utils/hooks/useNostr'; + +export function NewsfeedWidget() { + const { db } = useStorage(); + const { sub } = useNostr(); + const { relayUrls, ndk, fetcher } = useNDK(); + const { status, data } = useQuery({ + queryKey: ['newsfeed'], + queryFn: async ({ signal }: { signal: AbortSignal }) => { + const rootIds = new Set(); + const dedupQueue = new Set(); + + const events = await fetcher.fetchAllEvents( + relayUrls, + { + kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article], + authors: db.account.circles, + }, + { + since: db.account.last_login_at === 0 ? nHoursAgo(4) : db.account.last_login_at, + }, + { abortSignal: signal } + ); + + const ndkEvents = events.map((event) => { + return new NDKEvent(ndk, event); + }); + + ndkEvents.forEach((event) => { + const tags = event.tags.filter((el) => el[0] === 'e'); + if (tags && tags.length > 0) { + const rootId = tags.filter((el) => el[3] === 'root')[1] ?? tags[0][1]; + if (rootIds.has(rootId)) return dedupQueue.add(event.id); + rootIds.add(rootId); + } + }); + + return ndkEvents + .filter((event) => !dedupQueue.has(event.id)) + .sort((a, b) => b.created_at - a.created_at); + }, + }); + + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: async () => { + const currentLastEvent = data.at(-1); + const lastCreatedAt = currentLastEvent.created_at - 1; + + const rootIds = new Set(); + const dedupQueue = new Set(); + + const events = await fetcher.fetchLatestEvents( + relayUrls, + { + kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article], + authors: db.account.circles, + }, + 100, + { + asOf: lastCreatedAt, + } + ); + + const ndkEvents = events.map((event) => { + return new NDKEvent(ndk, event); + }); + + ndkEvents.forEach((event) => { + const tags = event.tags.filter((el) => el[0] === 'e'); + if (tags && tags.length > 0) { + const rootId = tags.filter((el) => el[3] === 'root')[1] ?? tags[0][1]; + if (rootIds.has(rootId)) return dedupQueue.add(event.id); + rootIds.add(rootId); + } + }); + + return ndkEvents + .filter((event) => !dedupQueue.has(event.id)) + .sort((a, b) => b.created_at - a.created_at); + }, + onSuccess: async (data) => { + queryClient.setQueryData(['newsfeed'], (old: NDKEvent[]) => [...old, ...data]); + }, + }); + + const renderItem = useCallback((event: NDKEvent) => { + switch (event.kind) { + case NDKKind.Text: + return ( + + + + ); + case NDKKind.Repost: + return ; + case 1063: + return ( + + + + ); + case NDKKind.Article: + return ( + + + + ); + default: + return ( + + + + ); + } + }, []); + + useEffect(() => { + if (db.account && db.account.circles.length > 0) { + const filter: NDKFilter = { + kinds: [NDKKind.Text, NDKKind.Repost], + authors: db.account.circles, + since: Math.floor(Date.now() / 1000), + }; + + sub(filter, async (event) => { + queryClient.setQueryData(['newsfeed'], (old: NDKEvent[]) => [event, ...old]); + }); + } + }, []); + + return ( + + + + {status === 'pending' ? ( +
+
+
+ +
+
+
+ +
+
+ ) : ( + data.map((item) => renderItem(item)) + )} +
+ {data ? ( + + ) : null} +
+
+
+ ); +} diff --git a/src/shared/widgets/local/notification.tsx b/src/shared/widgets/notification.tsx similarity index 93% rename from src/shared/widgets/local/notification.tsx rename to src/shared/widgets/notification.tsx index ce969d37..b0134c56 100644 --- a/src/shared/widgets/local/notification.tsx +++ b/src/shared/widgets/notification.tsx @@ -10,7 +10,6 @@ import { TitleBar } from '@shared/titleBar'; import { WidgetWrapper } from '@shared/widgets'; import { useActivities } from '@stores/activities'; -import { useWidgets } from '@stores/widgets'; import { useNostr } from '@utils/hooks/useNostr'; import { Widget } from '@utils/types'; @@ -24,8 +23,6 @@ export function LocalNotificationWidget({ params }: { params: Widget }) { state.setActivities, ]); - const isFetched = useWidgets((state) => state.isFetched); - const renderEvent = useCallback( (event: NDKEvent) => { if (event.pubkey === db.account.pubkey) return null; @@ -40,8 +37,8 @@ export function LocalNotificationWidget({ params }: { params: Widget }) { setActivities(events); } - if (isFetched) getActivities(); - }, [isFetched]); + getActivities(); + }, []); return ( diff --git a/src/stores/widgets.ts b/src/stores/widgets.ts index 1b6f3adb..d572b053 100644 --- a/src/stores/widgets.ts +++ b/src/stores/widgets.ts @@ -132,12 +132,14 @@ export const useWidgets = create()( fetchWidgets: async (db: LumeStorage) => { const dbWidgets = await db.getWidgets(); + /* dbWidgets.unshift({ id: '9998', title: 'Notification', content: '', kind: WidgetKinds.local.notification, }); + */ dbWidgets.unshift({ id: '9999', diff --git a/vite.config.ts b/vite.config.ts index 7d785431..c1c99d70 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,14 @@ import react from '@vitejs/plugin-react-swc'; -//import million from 'million/compiler'; +import million from 'million/compiler'; import { defineConfig } from 'vite'; import viteTsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ - plugins: [/*million.vite({ auto: true, mute: true }),*/ react(), viteTsconfigPaths()], + plugins: [ + million.vite({ optimize: false, auto: true, mute: true }), + react(), + viteTsconfigPaths(), + ], envPrefix: ['VITE_', 'TAURI_'], build: { target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13',