-
-
-
- Loading file sharing event...
-
-
+
+
- ) : dbEvents.length === 0 ? (
+ ) : allEvents.length === 0 ? (
- Oops, it looks like there are no file sharing events.
+ Oops, it looks like there are no files.
You can close this widget
@@ -70,38 +82,32 @@ export function LocalFilesWidget({ params }: { params: Widget }) {
) : (
-
- {dbEvents.map((item) => renderItem(item))}
-
- {dbEvents.length > 0 ? (
-
fetchNextPage()}
- disabled={!hasNextPage || isFetchingNextPage}
- className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
- >
- {isFetchingNextPage ? (
- <>
- Loading...
-
- >
- ) : hasNextPage ? (
- <>
-
- Load more
- >
- ) : (
- <>
-
- Nothing more to load
- >
- )}
-
- ) : null}
-
-
-
+ allEvents.map((item) => (
+
+
+
+ ))
)}
-
+
+ {hasNextPage ? (
+
fetchNextPage()}
+ disabled={!hasNextPage || isFetchingNextPage}
+ className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
+ >
+ {isFetchingNextPage ? (
+
+ ) : (
+ <>
+
+ Load more
+ >
+ )}
+
+ ) : null}
+
+
);
}
diff --git a/src/shared/widgets/local/follows.tsx b/src/shared/widgets/local/follows.tsx
deleted file mode 100644
index cb2836fb..00000000
--- a/src/shared/widgets/local/follows.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
-import { useInfiniteQuery } from '@tanstack/react-query';
-import { useCallback, useMemo } from 'react';
-import { VList } from 'virtua';
-
-import { useStorage } from '@libs/storage/provider';
-
-import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
-import {
- MemoizedArticleNote,
- MemoizedFileNote,
- MemoizedRepost,
- MemoizedTextNote,
- NoteWrapper,
- UnknownNote,
-} from '@shared/notes';
-import { TitleBar } from '@shared/titleBar';
-import { WidgetWrapper } from '@shared/widgets';
-
-import { DBEvent, Widget } from '@utils/types';
-
-export function LocalFollowsWidget({ params }: { params: Widget }) {
- const { db } = useStorage();
- const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
- useInfiniteQuery({
- queryKey: ['follows-' + params.title],
- initialPageParam: 0,
- queryFn: async ({ pageParam = 0 }) => {
- return await db.getAllEventsByAuthors(db.account.follows, 20, pageParam);
- },
- getNextPageParam: (lastPage) => lastPage.nextCursor,
- });
-
- 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]
- );
-
- return (
-
-
-
- {status === 'pending' ? (
-
-
-
-
- Loading post...
-
-
-
- ) : dbEvents.length === 0 ? (
-
-
-
-
-
- Oops, it looks like there are no posts.
-
-
- You can close this widget
-
-
-
-
- ) : (
-
- {dbEvents.map((item) => renderItem(item))}
-
- {dbEvents.length > 0 ? (
-
fetchNextPage()}
- disabled={!hasNextPage || isFetchingNextPage}
- className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
- >
- {isFetchingNextPage ? (
- <>
- Loading...
-
- >
- ) : hasNextPage ? (
- <>
-
- Load more
- >
- ) : (
- <>
-
- Nothing more to load
- >
- )}
-
- ) : null}
-
-
-
- )}
-
-
- );
-}
diff --git a/src/shared/widgets/local/user.tsx b/src/shared/widgets/local/user.tsx
index c436ebc6..4f6311f7 100644
--- a/src/shared/widgets/local/user.tsx
+++ b/src/shared/widgets/local/user.tsx
@@ -26,14 +26,30 @@ export function LocalUserWidget({ params }: { params: Widget }) {
const { status, data } = useQuery({
queryKey: ['user-posts', params.content],
queryFn: async () => {
+ const rootIds = new Set();
+ const dedupQueue = new Set();
+
const events = await ndk.fetchEvents({
// @ts-expect-error, NDK not support file metadata yet
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
authors: [params.content],
since: nHoursAgo(24),
});
- const sortedEvents = [...events].sort((x, y) => y.created_at - x.created_at);
- return sortedEvents;
+
+ const ndkEvents = [...events];
+
+ 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);
},
staleTime: Infinity,
refetchOnMount: false,
diff --git a/src/shared/widgets/newsfeed.tsx b/src/shared/widgets/newsfeed.tsx
index fa6a7f74..ab080eae 100644
--- a/src/shared/widgets/newsfeed.tsx
+++ b/src/shared/widgets/newsfeed.tsx
@@ -1,6 +1,6 @@
import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk';
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { useCallback, useEffect } from 'react';
+import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
+import { useCallback, useEffect, useMemo } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
@@ -19,92 +19,66 @@ import {
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 queryClient = useQueryClient();
+
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 { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
+ useInfiniteQuery({
+ queryKey: ['newsfeed'],
+ initialPageParam: 0,
+ queryFn: async ({
+ signal,
+ pageParam,
+ }: {
+ signal: AbortSignal;
+ pageParam: number;
+ }) => {
+ 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 events = await fetcher.fetchLatestEvents(
+ relayUrls,
+ {
+ kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
+ authors: db.account.circles,
+ },
+ 50,
+ { asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
+ );
- const ndkEvents = events.map((event) => {
- return new NDKEvent(ndk, event);
- });
+ 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);
- }
- });
+ 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);
- },
- });
+ return ndkEvents
+ .filter((event) => !dedupQueue.has(event.id))
+ .sort((a, b) => b.created_at - a.created_at);
+ },
+ getNextPageParam: (lastPage) => {
+ const lastEvent = lastPage.at(-1);
+ if (!lastEvent) return;
+ return lastEvent.created_at - 1;
+ },
+ });
- 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 allEvents = useMemo(
+ () => (data ? data.pages.flatMap((page) => page) : []),
+ [data]
+ );
const renderItem = useCallback((event: NDKEvent) => {
switch (event.kind) {
@@ -138,46 +112,55 @@ export function NewsfeedWidget() {
}, []);
useEffect(() => {
- if (db.account && db.account.circles.length > 0) {
+ if (status === 'success' && db.account && db.account.circles.length > 0) {
+ queryClient.fetchQuery({ queryKey: ['notification'] });
+
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]);
- });
+ sub(
+ filter,
+ async (event) => {
+ queryClient.setQueryData(['newsfeed'], (old: NDKEvent[]) => [event, ...old]);
+ },
+ false,
+ 'newsfeed'
+ );
}
- }, []);
+ }, [status]);
return (
{status === 'pending' ? (
-
-
-
-
+
) : (
- data.map((item) => renderItem(item))
+ allEvents.map((item) => renderItem(item))
)}
- {data ? (
+ {hasNextPage ? (
mutation.mutate()}
+ onClick={() => fetchNextPage()}
+ disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
>
-
- Load more
+ {isFetchingNextPage ? (
+
+ ) : (
+ <>
+
+ Load more
+ >
+ )}
) : null}
diff --git a/src/shared/widgets/notification.tsx b/src/shared/widgets/notification.tsx
index b0134c56..104226d5 100644
--- a/src/shared/widgets/notification.tsx
+++ b/src/shared/widgets/notification.tsx
@@ -1,59 +1,145 @@
-import { NDKEvent } from '@nostr-dev-kit/ndk';
-import { useCallback, useEffect } from 'react';
+import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
+import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
+import { useCallback, useEffect, useMemo } from 'react';
import { VList } from 'virtua';
+import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
-import { LoaderIcon } from '@shared/icons';
+import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
+import { NoteSkeleton } from '@shared/notes';
import { NotifyNote } from '@shared/notification/notifyNote';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
-import { useActivities } from '@stores/activities';
-
import { useNostr } from '@utils/hooks/useNostr';
-import { Widget } from '@utils/types';
+import { sendNativeNotification } from '@utils/notification';
+
+export function NotificationWidget() {
+ const queryClient = useQueryClient();
-export function LocalNotificationWidget({ params }: { params: Widget }) {
const { db } = useStorage();
- const { getAllActivities } = useNostr();
+ const { sub } = useNostr();
+ const { ndk, relayUrls, fetcher } = useNDK();
+ const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
+ useInfiniteQuery({
+ queryKey: ['notification'],
+ initialPageParam: 0,
+ queryFn: async ({
+ signal,
+ pageParam,
+ }: {
+ signal: AbortSignal;
+ pageParam: number;
+ }) => {
+ const events = await fetcher.fetchLatestEvents(
+ relayUrls,
+ {
+ kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
+ '#p': [db.account.pubkey],
+ },
+ 50,
+ { asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
+ );
- const [activities, setActivities] = useActivities((state) => [
- state.activities,
- state.setActivities,
- ]);
+ const ndkEvents = events.map((event) => {
+ return new NDKEvent(ndk, event);
+ });
- const renderEvent = useCallback(
- (event: NDKEvent) => {
- if (event.pubkey === db.account.pubkey) return null;
- return
;
- },
- [activities]
+ return ndkEvents.sort((a, b) => b.created_at - a.created_at);
+ },
+ getNextPageParam: (lastPage) => {
+ const lastEvent = lastPage.at(-1);
+ if (!lastEvent) return;
+ return lastEvent.created_at - 1;
+ },
+ enabled: false,
+ });
+
+ const allEvents = useMemo(
+ () => (data ? data.pages.flatMap((page) => page) : []),
+ [data]
);
- useEffect(() => {
- async function getActivities() {
- const events = await getAllActivities(48);
- setActivities(events);
- }
-
- getActivities();
+ const renderEvent = useCallback((event: NDKEvent) => {
+ if (event.pubkey === db.account.pubkey) return null;
+ return
;
}, []);
+ useEffect(() => {
+ if (status === 'success' && db.account) {
+ const filter = {
+ kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
+ '#p': [db.account.pubkey],
+ since: Math.floor(Date.now() / 1000),
+ };
+
+ sub(
+ filter,
+ async (event) => {
+ queryClient.setQueryData(['notification'], (old: NDKEvent[]) => [
+ event,
+ ...old,
+ ]);
+
+ const user = ndk.getUser({ hexpubkey: event.pubkey });
+ await user.fetchProfile();
+
+ switch (event.kind) {
+ case NDKKind.Text:
+ return await sendNativeNotification(
+ `${
+ user.profile.displayName || user.profile.name
+ } has replied to your note`
+ );
+ case NDKKind.EncryptedDirectMessage: {
+ if (location.pathname !== '/chats') {
+ return await sendNativeNotification(
+ `${
+ user.profile.displayName || user.profile.name
+ } has send you a encrypted message`
+ );
+ } else {
+ break;
+ }
+ }
+ case NDKKind.Repost:
+ return await sendNativeNotification(
+ `${
+ user.profile.displayName || user.profile.name
+ } has reposted to your note`
+ );
+ case NDKKind.Reaction:
+ return await sendNativeNotification(
+ `${user.profile.displayName || user.profile.name} has reacted ${
+ event.content
+ } to your note`
+ );
+ case NDKKind.Zap:
+ return await sendNativeNotification(
+ `${user.profile.displayName || user.profile.name} has zapped to your note`
+ );
+ default:
+ break;
+ }
+ },
+ false,
+ 'notification'
+ );
+ }
+ }, [status]);
+
return (
-
-
- {!activities ? (
-
-
-
-
- Loading...
-
+
+
+ {status === 'pending' ? (
+
- ) : activities.length < 1 ? (
+ ) : allEvents.length < 1 ? (
๐
@@ -61,12 +147,28 @@ export function LocalNotificationWidget({ params }: { params: Widget }) {
) : (
-
- {activities.map((event) => renderEvent(event))}
-
-
+ allEvents.map((event) => renderEvent(event))
)}
-
+
+ {hasNextPage ? (
+
fetchNextPage()}
+ disabled={!hasNextPage || isFetchingNextPage}
+ className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
+ >
+ {isFetchingNextPage ? (
+
+ ) : (
+ <>
+
+ Load more
+ >
+ )}
+
+ ) : null}
+
+
);
}
diff --git a/src/shared/widgets/other/learnNostr.tsx b/src/shared/widgets/other/learnNostr.tsx
deleted file mode 100644
index 0888a746..00000000
--- a/src/shared/widgets/other/learnNostr.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { useNavigate } from 'react-router-dom';
-
-import { ArrowRightIcon } from '@shared/icons';
-import { TitleBar } from '@shared/titleBar';
-import { WidgetWrapper } from '@shared/widgets';
-
-import { useResources } from '@stores/resources';
-
-import { Widget } from '@utils/types';
-
-export function LearnNostrWidget({ params }: { params: Widget }) {
- const navigate = useNavigate();
- const openResource = useResources((state) => state.openResource);
- const resources = useResources((state) => state.resources);
- const seens = useResources((state) => state.seens);
-
- const open = (naddr: string) => {
- // add resource to seen list
- openResource(naddr);
- // redirect
- navigate(`/notes/article/${naddr}`);
- };
-
- return (
-
-
-
- {resources.map((resource, index) => (
-
-
- {resource.title}
-
-
- {resource.data.length ? (
- resource.data.map((item, index) => (
-
open(item.id)}
- className="flex items-center justify-between rounded-xl bg-neutral-100 px-4 py-3 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
- >
-
-
- {item.title}
-
- {seens.has(item.id) ? (
-
Readed
- ) : (
-
- Unread
-
- )}
-
-
-
- ))
- ) : (
-
-
- More resources are coming, stay tuned.
-
-
- )}
-
-
- ))}
-
-
- );
-}
diff --git a/src/shared/widgets/tmp/feeds.tsx b/src/shared/widgets/tmp/feeds.tsx
index d046c159..2178a404 100644
--- a/src/shared/widgets/tmp/feeds.tsx
+++ b/src/shared/widgets/tmp/feeds.tsx
@@ -5,17 +5,15 @@ import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, CheckCircleIcon } from '@shared/icons';
import { User } from '@shared/user';
-import { WidgetKinds, useWidgets } from '@stores/widgets';
+import { WidgetKinds } from '@stores/constants';
+import { useWidget } from '@utils/hooks/useWidget';
import { Widget } from '@utils/types';
export function XfeedsWidget({ params }: { params: Widget }) {
const { db } = useStorage();
+ const { addWidget, removeWidget } = useWidget();
- const [setWidget, removeWidget] = useWidgets((state) => [
- state.setWidget,
- state.removeWidget,
- ]);
const [title, setTitle] = useState
('');
const [groups, setGroups] = useState>([]);
@@ -28,17 +26,17 @@ export function XfeedsWidget({ params }: { params: Widget }) {
};
const cancel = () => {
- removeWidget(db, params.id);
+ removeWidget.mutate(params.id);
};
const submit = async () => {
- setWidget(db, {
+ addWidget.mutate({
kind: WidgetKinds.local.feeds,
title: title || 'Group',
content: JSON.stringify(groups),
});
// remove temp widget
- removeWidget(db, params.id);
+ removeWidget.mutate(params.id);
};
return (
diff --git a/src/shared/widgets/tmp/hashtag.tsx b/src/shared/widgets/tmp/hashtag.tsx
index 973dd9a7..278abd4a 100644
--- a/src/shared/widgets/tmp/hashtag.tsx
+++ b/src/shared/widgets/tmp/hashtag.tsx
@@ -1,11 +1,10 @@
import { Resolver, useForm } from 'react-hook-form';
-import { useStorage } from '@libs/storage/provider';
-
import { ArrowRightCircleIcon } from '@shared/icons';
-import { WidgetKinds, useWidgets } from '@stores/widgets';
+import { WidgetKinds } from '@stores/constants';
+import { useWidget } from '@utils/hooks/useWidget';
import { Widget } from '@utils/types';
type FormValues = {
@@ -27,12 +26,7 @@ const resolver: Resolver = async (values) => {
};
export function XhashtagWidget({ params }: { params: Widget }) {
- const [setWidget, removeWidget] = useWidgets((state) => [
- state.setWidget,
- state.removeWidget,
- ]);
-
- const { db } = useStorage();
+ const { addWidget, removeWidget } = useWidget();
const {
register,
setError,
@@ -41,18 +35,18 @@ export function XhashtagWidget({ params }: { params: Widget }) {
} = useForm({ resolver });
const cancel = () => {
- removeWidget(db, params.id);
+ removeWidget.mutate(params.id);
};
const onSubmit = async (data: FormValues) => {
try {
- setWidget(db, {
+ addWidget.mutate({
kind: WidgetKinds.global.hashtag,
title: data.hashtag,
content: data.hashtag.replace('#', ''),
});
// remove temp widget
- removeWidget(db, params.id);
+ removeWidget.mutate(params.id);
} catch (e) {
setError('hashtag', {
type: 'custom',
diff --git a/src/stores/activities.ts b/src/stores/activities.ts
deleted file mode 100644
index 0265a9e5..00000000
--- a/src/stores/activities.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { NDKEvent } from '@nostr-dev-kit/ndk';
-import { create } from 'zustand';
-
-interface ActivitiesState {
- activities: Array;
- newMessages: number;
- setActivities: (events: NDKEvent[]) => void;
- addActivity: (event: NDKEvent) => void;
- addNewMessage: () => void;
- clearNewMessage: () => void;
-}
-
-export const useActivities = create((set) => ({
- activities: null,
- newMessages: 0,
- setActivities: (events: NDKEvent[]) => {
- set(() => ({
- activities: events,
- }));
- },
- addActivity: (event: NDKEvent) => {
- set((state) => ({
- activities: state.activities ? [event, ...state.activities] : [event],
- }));
- },
- addNewMessage: () => {
- set((state) => ({ newMessages: state.newMessages + 1 }));
- },
- clearNewMessage: () => {
- set(() => ({ newMessages: 0 }));
- },
-}));
diff --git a/src/stores/constants.ts b/src/stores/constants.ts
index 5a76cdfc..46bc3f2c 100644
--- a/src/stores/constants.ts
+++ b/src/stores/constants.ts
@@ -1,3 +1,5 @@
+import { WidgetGroup } from '@utils/types';
+
export const FULL_RELAYS = [
'wss://relay.damus.io',
'wss://relay.primal.net',
@@ -5,3 +7,104 @@ export const FULL_RELAYS = [
'wss://relay.nostr.band/all',
'wss://nostr.mutinywallet.com',
];
+
+export const FETCH_LIMIT = 50;
+
+export const WidgetKinds = {
+ local: {
+ network: 100,
+ feeds: 101,
+ files: 102,
+ articles: 103,
+ user: 104,
+ thread: 105,
+ follows: 106,
+ notification: 107,
+ },
+ global: {
+ feeds: 1000,
+ files: 1001,
+ articles: 1002,
+ hashtag: 1003,
+ },
+ nostrBand: {
+ trendingAccounts: 1,
+ trendingNotes: 2,
+ },
+ other: {
+ learnNostr: 90000,
+ },
+ tmp: {
+ list: 10000,
+ xfeed: 10001,
+ xhashtag: 10002,
+ },
+};
+
+export const DefaultWidgets: Array = [
+ {
+ title: 'Circles / Follows',
+ data: [
+ {
+ kind: WidgetKinds.tmp.xfeed,
+ title: 'Group feeds',
+ description: 'All posts from specific people you want to keep up with',
+ },
+ {
+ kind: WidgetKinds.local.files,
+ title: 'Files',
+ description: 'All files shared by people in your circle',
+ },
+ {
+ kind: WidgetKinds.local.articles,
+ title: 'Articles',
+ description: 'All articles shared by people in your circle',
+ },
+ ],
+ },
+ {
+ title: 'Global',
+ data: [
+ {
+ kind: WidgetKinds.tmp.xhashtag,
+ title: 'Hashtag',
+ description: 'All posts have a specific hashtag',
+ },
+ {
+ kind: WidgetKinds.global.files,
+ title: 'Files',
+ description: 'All files shared by people in your current relay set',
+ },
+ {
+ kind: WidgetKinds.global.articles,
+ title: 'Articles',
+ description: 'All articles shared by people in your current relay set',
+ },
+ ],
+ },
+ {
+ title: 'nostr.band',
+ data: [
+ {
+ kind: WidgetKinds.nostrBand.trendingAccounts,
+ title: 'Accounts',
+ description: 'Trending accounts from the last 24 hours',
+ },
+ {
+ kind: WidgetKinds.nostrBand.trendingNotes,
+ title: 'Notes',
+ description: 'Trending notes from the last 24 hours',
+ },
+ ],
+ },
+ {
+ title: 'Other',
+ data: [
+ {
+ kind: WidgetKinds.local.notification,
+ title: 'Notification',
+ description: 'Everything happens around you',
+ },
+ ],
+ },
+];
diff --git a/src/stores/widgets.ts b/src/stores/widgets.ts
deleted file mode 100644
index d572b053..00000000
--- a/src/stores/widgets.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-import { create } from 'zustand';
-import { createJSONStorage, persist } from 'zustand/middleware';
-
-import { LumeStorage } from '@libs/storage/instance';
-
-import { Widget, WidgetGroup } from '@utils/types';
-
-interface WidgetState {
- widgets: null | Array;
- isFetched: boolean;
- fetchWidgets: (db: LumeStorage) => void;
- setWidget: (db: LumeStorage, { kind, title, content }: Widget) => void;
- removeWidget: (db: LumeStorage, id: string) => void;
- reorderWidget: (id: string, position: number) => void;
- setIsFetched: () => void;
-}
-
-export const WidgetKinds = {
- local: {
- network: 100,
- feeds: 101,
- files: 102,
- articles: 103,
- user: 104,
- thread: 105,
- follows: 106,
- notification: 107,
- },
- global: {
- feeds: 1000,
- files: 1001,
- articles: 1002,
- hashtag: 1003,
- },
- nostrBand: {
- trendingAccounts: 1,
- trendingNotes: 2,
- },
- other: {
- learnNostr: 90000,
- },
- tmp: {
- list: 10000,
- xfeed: 10001,
- xhashtag: 10002,
- },
-};
-
-export const DefaultWidgets: Array = [
- {
- title: 'Circles / Follows',
- data: [
- {
- kind: WidgetKinds.tmp.xfeed,
- title: 'Group feeds',
- description: 'All posts from specific people you want to keep up with',
- },
- {
- kind: WidgetKinds.local.files,
- title: 'Files',
- description: 'All files shared by people in your circle',
- },
- {
- kind: WidgetKinds.local.articles,
- title: 'Articles',
- description: 'All articles shared by people in your circle',
- },
- {
- kind: WidgetKinds.local.follows,
- title: 'Follows',
- description: 'All posts from people you are following',
- },
- ],
- },
- {
- title: 'Global',
- data: [
- {
- kind: WidgetKinds.tmp.xhashtag,
- title: 'Hashtag',
- description: 'All posts have a specific hashtag',
- },
- {
- kind: WidgetKinds.global.files,
- title: 'Files',
- description: 'All files shared by people in your current relay set',
- },
- {
- kind: WidgetKinds.global.articles,
- title: 'Articles',
- description: 'All articles shared by people in your current relay set',
- },
- ],
- },
- {
- title: 'nostr.band',
- data: [
- {
- kind: WidgetKinds.nostrBand.trendingAccounts,
- title: 'Accounts',
- description: 'Trending accounts from the last 24 hours',
- },
- {
- kind: WidgetKinds.nostrBand.trendingNotes,
- title: 'Notes',
- description: 'Trending notes from the last 24 hours',
- },
- ],
- },
- {
- title: 'Other',
- data: [
- {
- kind: WidgetKinds.local.notification,
- title: 'Notification',
- description: 'Everything happens around you',
- },
- {
- kind: WidgetKinds.other.learnNostr,
- title: 'Learn Nostr',
- description: 'All things you need to know about Nostr',
- },
- ],
- },
-];
-
-export const useWidgets = create()(
- persist(
- (set) => ({
- widgets: null,
- isFetched: false,
- fetchWidgets: async (db: LumeStorage) => {
- const dbWidgets = await db.getWidgets();
-
- /*
- dbWidgets.unshift({
- id: '9998',
- title: 'Notification',
- content: '',
- kind: WidgetKinds.local.notification,
- });
- */
-
- dbWidgets.unshift({
- id: '9999',
- title: '',
- content: '',
- kind: WidgetKinds.local.network,
- });
-
- set({ widgets: dbWidgets });
- },
- setWidget: async (db: LumeStorage, { kind, title, content }: Widget) => {
- const widget: Widget = await db.createWidget(kind, title, content);
- set((state) => ({ widgets: [...state.widgets, widget] }));
- },
- removeWidget: async (db: LumeStorage, id: string) => {
- await db.removeWidget(id);
- set((state) => ({ widgets: state.widgets.filter((widget) => widget.id !== id) }));
- },
- reorderWidget: (id: string, position: number) => {
- set((state) => {
- const widgets = [...state.widgets];
- const widget = widgets.find((widget) => widget.id === id);
- if (!widget) return { widgets };
-
- const idx = widgets.indexOf(widget);
- widgets.splice(idx, 1);
- widgets.splice(position, 0, widget);
-
- return { widgets };
- });
- },
- setIsFetched: () => {
- set({ isFetched: true });
- },
- }),
- {
- name: 'widgets',
- storage: createJSONStorage(() => sessionStorage),
- }
- )
-);
diff --git a/src/utils/hooks/useNostr.ts b/src/utils/hooks/useNostr.ts
index ed44e08e..00145054 100644
--- a/src/utils/hooks/useNostr.ts
+++ b/src/utils/hooks/useNostr.ts
@@ -29,11 +29,12 @@ export function useNostr() {
const sub = async (
filter: NDKFilter,
callback: (event: NDKEvent) => void,
- groupable?: boolean
+ groupable?: boolean,
+ subKey?: string
) => {
if (!ndk) throw new Error('NDK instance not found');
- const key = JSON.stringify(filter);
+ const key = subKey ?? JSON.stringify(filter);
if (!subManager.get(key)) {
const subEvent = ndk.subscribe(filter, {
closeOnEose: false,
diff --git a/src/utils/hooks/useWidget.ts b/src/utils/hooks/useWidget.ts
new file mode 100644
index 00000000..b2203162
--- /dev/null
+++ b/src/utils/hooks/useWidget.ts
@@ -0,0 +1,30 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { useStorage } from '@libs/storage/provider';
+
+import { Widget } from '@utils/types';
+
+export function useWidget() {
+ const { db } = useStorage();
+ const queryClient = useQueryClient();
+
+ const addWidget = useMutation({
+ mutationFn: async (widget: Widget) => {
+ return await db.createWidget(widget.kind, widget.title, widget.content);
+ },
+ onSuccess: (data) => {
+ queryClient.setQueryData(['widgets'], (old: Widget[]) => [...old, data]);
+ },
+ });
+
+ const removeWidget = useMutation({
+ mutationFn: async (id: string) => {
+ return await db.removeWidget(id);
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['widgets'] });
+ },
+ });
+
+ return { addWidget, removeWidget };
+}