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