add topic widget

This commit is contained in:
2023-11-09 09:12:42 +07:00
parent 108ecafab7
commit cb9006abb2
16 changed files with 489 additions and 96 deletions

View File

@@ -14,3 +14,4 @@ export * from './tmp/hashtag';
export * from './newsfeed';
export * from './notification';
export * from './liveUpdater';
export * from './topic';

View File

@@ -6,7 +6,7 @@ import { ArrowRightCircleIcon, CancelIcon, CheckCircleIcon } from '@shared/icons
import { User } from '@shared/user';
import { WidgetWrapper } from '@shared/widgets';
import { WidgetKinds } from '@stores/constants';
import { WIDGET_KIND } from '@stores/constants';
import { useWidget } from '@utils/hooks/useWidget';
import { Widget } from '@utils/types';
@@ -28,7 +28,7 @@ export function XfeedsWidget({ params }: { params: Widget }) {
const submit = async () => {
addWidget.mutate({
kind: WidgetKinds.local.feeds,
kind: WIDGET_KIND.local.feeds,
title: title || 'Group',
content: JSON.stringify(groups),
});

View File

@@ -3,7 +3,7 @@ import { Resolver, useForm } from 'react-hook-form';
import { ArrowRightCircleIcon, CancelIcon } from '@shared/icons';
import { WidgetWrapper } from '@shared/widgets';
import { HASHTAGS, WidgetKinds } from '@stores/constants';
import { HASHTAGS, WIDGET_KIND } from '@stores/constants';
import { useWidget } from '@utils/hooks/useWidget';
import { Widget } from '@utils/types';
@@ -39,7 +39,7 @@ export function XhashtagWidget({ params }: { params: Widget }) {
const onSubmit = async (data: FormValues) => {
try {
addWidget.mutate({
kind: WidgetKinds.global.hashtag,
kind: WIDGET_KIND.global.hashtag,
title: data.hashtag,
content: data.hashtag.replace('#', ''),
});

View File

@@ -0,0 +1,128 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import {
MemoizedRepost,
MemoizedTextNote,
NoteSkeleton,
UnknownNote,
} from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@stores/constants';
import { Widget } from '@utils/types';
export function TopicWidget({ widget }: { widget: Widget }) {
const { relayUrls, ndk, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: [widget.title],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const hashtags: string[] = JSON.parse(widget.content as string);
const rootIds = new Set();
const dedupQueue = new Set();
const events = await fetcher.fetchLatestEvents(
relayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost],
'#t': hashtags,
},
FETCH_LIMIT,
{ asOf: pageParam === 0 ? undefined : pageParam, 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);
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data]
);
const renderItem = useCallback(
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <MemoizedTextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <MemoizedRepost key={event.id} event={event} />;
default:
return <UnknownNote key={event.id} event={event} />;
}
},
[data]
);
return (
<WidgetWrapper>
<TitleBar id={widget.id} title={widget.title} />
<VList className="flex-1" overscan={2}>
{status === 'pending' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
<NoteSkeleton />
</div>
</div>
) : (
allEvents.map((item) => renderItem(item))
)}
<div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (
<button
type="button"
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"
>
{isFetchingNextPage ? (
<LoaderIcon className="h-4 w-4 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList>
</WidgetWrapper>
);
}