refactor widget

This commit is contained in:
Ren Amamiya 2023-08-30 16:21:42 +07:00
parent abe4d11498
commit aced6077bd
27 changed files with 544 additions and 246 deletions

View File

@ -1,163 +0,0 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { useStorage } from '@libs/storage/provider';
import {
AddWidgetIcon,
FeedIcon,
FileIcon,
HashtagIcon,
ThreadsIcon,
TrendingIcon,
} from '@shared/icons';
import { WidgetKinds, useWidgets } from '@stores/widgets';
export function AddWidgetButton() {
const { db } = useStorage();
const setWidget = useWidgets((state) => state.setWidget);
const setTrendingProfilesWidget = () => {
setWidget(db, {
kind: WidgetKinds.trendingProfiles,
title: 'Trending Profiles',
content: 'https://api.nostr.band/v0/trending/profiles',
});
};
const setTrendingNotesWidget = () => {
setWidget(db, {
kind: WidgetKinds.trendingNotes,
title: 'Trending Notes',
content: 'https://api.nostr.band/v0/trending/notes',
});
};
const setArticleWidget = () => {
setWidget(db, {
kind: WidgetKinds.article,
title: 'Articles',
content: '',
});
};
const setFileWidget = () => {
setWidget(db, {
kind: WidgetKinds.file,
title: 'Files',
content: '',
});
};
const setHashtagWidget = () => {
setWidget(db, {
kind: WidgetKinds.xhashtag,
title: 'New hashtag',
content: '',
});
};
const setGroupFeedWidget = () => {
setWidget(db, {
kind: WidgetKinds.xfeed,
title: 'New user group feed',
content: '',
});
};
return (
<DropdownMenu.Root>
<div className="flex h-full shrink-0 grow-0 basis-[400px] flex-col">
<div className="inline-flex h-full w-full flex-col items-center justify-center">
<DropdownMenu.Trigger asChild>
<button type="button" className="flex flex-col items-center gap-2">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-white/5 backdrop-blur-xl hover:bg-white/10">
<AddWidgetIcon className="h-5 w-5 text-white" />
</div>
<p className="font-medium text-white/50">Add widget</p>
</button>
</DropdownMenu.Trigger>
</div>
</div>
<DropdownMenu.Portal>
<DropdownMenu.Content
sideOffset={-20}
className="flex w-[256px] flex-col overflow-hidden rounded-md bg-white/10 p-2 backdrop-blur-3xl focus:outline-none"
>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={setHashtagWidget}
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<HashtagIcon className="h-4 w-4 text-white" />
</div>
Add hashtag feeds
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={setGroupFeedWidget}
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<FeedIcon className="h-4 w-4 text-white" />
</div>
Add user group feeds
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={setArticleWidget}
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<ThreadsIcon className="h-4 w-4 text-white" />
</div>
Add article feeds
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={setFileWidget}
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
>
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<FileIcon className="h-4 w-4 text-white" />
</div>
Add file feeds
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={setTrendingProfilesWidget}
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<TrendingIcon className="h-4 w-4 text-white" />
</div>
Add trending accounts
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={setTrendingNotesWidget}
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<TrendingIcon className="h-4 w-4 text-white" />
</div>
Add trending notes
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}

View File

@ -0,0 +1,25 @@
import { useStorage } from '@libs/storage/provider';
import { PlusIcon } from '@shared/icons';
import { WidgetKinds, useWidgets } from '@stores/widgets';
export function ToggleWidgetList() {
const { db } = useStorage();
const setWidget = useWidgets((state) => state.setWidget);
return (
<div className="relative flex h-full shrink-0 grow-0 basis-[400px] items-center justify-center">
<button
type="button"
onClick={() =>
setWidget(db, { kind: WidgetKinds.tmp.list, title: '', content: '' })
}
className="inline-flex items-center gap-2 text-white"
>
<PlusIcon className="h-4 w-4 text-white" />
<p className="text-sm font-bold leading-none">Add widget</p>
</button>
</div>
);
}

View File

@ -0,0 +1,51 @@
import { useCallback } from 'react';
import { useStorage } from '@libs/storage/provider';
import { TitleBar } from '@shared/titleBar';
import { DefaultWidgets, useWidgets } from '@stores/widgets';
import { Widget, WidgetGroup, WidgetGroupItem } from '@utils/types';
export function WidgetList({ params }: { params: Widget }) {
const { db } = useStorage();
const [setWidget, removeWidget] = useWidgets((state) => [
state.setWidget,
state.removeWidget,
]);
const openWidget = (widget: WidgetGroupItem) => {
setWidget(db, { kind: widget.kind, title: widget.title, content: '' });
removeWidget(db, params.id);
};
const renderItem = useCallback(
(row: WidgetGroup) => {
return (
<div className="flex flex-col gap-3">
<h3 className="font-medium text-white/50">{row.title}</h3>
<div className="grid grid-cols-3 gap-6">
{row.data.map((item, index) => (
<button onClick={() => openWidget(item)} key={index}>
<div className="inline-flex aspect-square h-full w-full transform-gpu flex-col items-center justify-center gap-2.5 rounded-2xl bg-white/5 hover:bg-white/10">
<h5 className="text-sm font-medium">{item.title}</h5>
</div>
</button>
))}
</div>
</div>
);
},
[DefaultWidgets]
);
return (
<div className="relative h-full shrink-0 grow-0 basis-[400px] overflow-hidden">
<TitleBar title="Add widget" />
<div className="flex flex-col gap-8 px-3">
{DefaultWidgets.map((row: WidgetGroup) => renderItem(row))}
</div>
</div>
);
}

View File

@ -1,21 +1,26 @@
import { useCallback, useEffect } from 'react';
import { AddWidgetButton } from '@app/space/components/button';
import { FeedWidgetForm } from '@app/space/components/forms/feed';
import { HashTagWidgetForm } from '@app/space/components/forms/hashtag';
import { ArticleWidget } from '@app/space/components/widgets/article';
import { FeedWidget } from '@app/space/components/widgets/feed';
import { FileWidget } from '@app/space/components/widgets/file';
import { HashtagWidget } from '@app/space/components/widgets/hashtag';
import { NetworkWidget } from '@app/space/components/widgets/network';
import { ThreadBlock } from '@app/space/components/widgets/thread';
import { TrendingNotesWidget } from '@app/space/components/widgets/trendingNotes';
import { TrendingProfilesWidget } from '@app/space/components/widgets/trendingProfile';
import { UserWidget } from '@app/space/components/widgets/user';
import { ToggleWidgetList } from '@app/space/components/toggle';
import { WidgetList } from '@app/space/components/widgetList';
import { useStorage } from '@libs/storage/provider';
import { LoaderIcon } from '@shared/icons';
import {
GlobalArticlesWidget,
GlobalFilesWidget,
GlobalHashtagWidget,
LocalArticlesWidget,
LocalFeedsWidget,
LocalFilesWidget,
LocalNetworkWidget,
LocalThreadWidget,
LocalUserWidget,
TrendingAccountsWidget,
TrendingNotesWidget,
XfeedsWidget,
XhashtagWidget,
} from '@shared/widgets';
import { WidgetKinds, useWidgets } from '@stores/widgets';
@ -33,28 +38,34 @@ export function SpaceScreen() {
(widget: Widget) => {
if (!widget) return;
switch (widget.kind) {
case WidgetKinds.feed:
return <FeedWidget key={widget.id} params={widget} />;
case WidgetKinds.thread:
return <ThreadBlock key={widget.id} params={widget} />;
case WidgetKinds.hashtag:
return <HashtagWidget key={widget.id} params={widget} />;
case WidgetKinds.user:
return <UserWidget key={widget.id} params={widget} />;
case WidgetKinds.trendingProfiles:
return <TrendingProfilesWidget key={widget.id} params={widget} />;
case WidgetKinds.trendingNotes:
case WidgetKinds.local.network:
return <LocalNetworkWidget key={widget.id} />;
case WidgetKinds.local.feeds:
return <LocalFeedsWidget key={widget.id} params={widget} />;
case WidgetKinds.local.files:
return <LocalFilesWidget key={widget.id} params={widget} />;
case WidgetKinds.local.articles:
return <LocalArticlesWidget key={widget.id} params={widget} />;
case WidgetKinds.local.user:
return <LocalUserWidget key={widget.id} params={widget} />;
case WidgetKinds.local.thread:
return <LocalThreadWidget key={widget.id} params={widget} />;
case WidgetKinds.global.hashtag:
return <GlobalHashtagWidget key={widget.id} params={widget} />;
case WidgetKinds.global.articles:
return <GlobalArticlesWidget key={widget.id} params={widget} />;
case WidgetKinds.global.files:
return <GlobalFilesWidget key={widget.id} params={widget} />;
case WidgetKinds.nostrBand.trendingAccounts:
return <TrendingAccountsWidget key={widget.id} params={widget} />;
case WidgetKinds.nostrBand.trendingNotes:
return <TrendingNotesWidget key={widget.id} params={widget} />;
case WidgetKinds.network:
return <NetworkWidget key={widget.id} />;
case WidgetKinds.article:
return <ArticleWidget key={widget.id} params={widget} />;
case WidgetKinds.file:
return <FileWidget key={widget.id} params={widget} />;
case WidgetKinds.xhashtag:
return <HashTagWidgetForm key={widget.id} params={widget} />;
case WidgetKinds.xfeed:
return <FeedWidgetForm key={widget.id} params={widget} />;
case WidgetKinds.tmp.xfeed:
return <XhashtagWidget key={widget.id} params={widget} />;
case WidgetKinds.tmp.xhashtag:
return <XfeedsWidget key={widget.id} params={widget} />;
case WidgetKinds.tmp.list:
return <WidgetList key={widget.id} params={widget} />;
default:
break;
}
@ -77,7 +88,7 @@ export function SpaceScreen() {
) : (
widgets.map((widget) => renderItem(widget))
)}
<AddWidgetButton />
<ToggleWidgetList />
</div>
);
}

View File

@ -223,6 +223,35 @@ export class LumeStorage {
};
}
public async getAllEventsByKinds(kinds: number[], limit: number, offset: number) {
const totalEvents = await this.countTotalEvents();
const nextCursor = offset + limit;
const authorsArr = `'${kinds.join("','")}'`;
const events: { data: DBEvent[] | null; nextCursor: number } = {
data: null,
nextCursor: 0,
};
const query: DBEvent[] = await this.db.select(
`SELECT * FROM events WHERE kinds IN (${authorsArr}) ORDER BY created_at DESC LIMIT $1 OFFSET $2;`,
[limit, offset]
);
if (query && query.length > 0) {
events['data'] = query;
events['nextCursor'] =
Math.round(totalEvents / nextCursor) > 1 ? nextCursor : undefined;
return events;
}
return {
data: [],
nextCursor: 0,
};
}
public async isEventsEmpty() {
const results: DBEvent[] = await this.db.select(
'SELECT * FROM events ORDER BY id DESC LIMIT 1;'

View File

@ -20,7 +20,7 @@ export function Repost({ event }: { event: NDKEvent }) {
if (status === 'loading') {
return (
<div className="h-min w-full px-3 py-1.5">
<div className="h-min w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
@ -30,7 +30,7 @@ export function Repost({ event }: { event: NDKEvent }) {
if (status === 'error') {
return (
<div className="h-min w-full px-3 py-1.5">
<div className="h-min w-full px-3 pb-3">
<div className="flex flex-col gap-1 overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<p className="select-text break-all text-white/50">
Failed to get repost with ID
@ -57,7 +57,7 @@ export function Repost({ event }: { event: NDKEvent }) {
};
return (
<div className="h-min w-full px-3 py-1.5">
<div className="h-min w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
<div className="relative flex flex-col">
<div className="isolate flex flex-col -space-y-4">

View File

@ -11,7 +11,7 @@ export function Hashtag({ tag }: { tag: string }) {
type="button"
onClick={() =>
setWidget(db, {
kind: WidgetKinds.hashtag,
kind: WidgetKinds.global.hashtag,
title: tag,
content: tag.replace('#', ''),
})

View File

@ -25,7 +25,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
const openThread = (event, thread: string) => {
const selection = window.getSelection();
if (selection.toString().length === 0) {
setWidget(db, { kind: WidgetKinds.thread, title: 'Thread', content: thread });
setWidget(db, { kind: WidgetKinds.local.thread, title: 'Thread', content: thread });
} else {
event.stopPropagation();
}

View File

@ -16,7 +16,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
type="button"
onClick={() =>
setWidget(db, {
kind: WidgetKinds.user,
kind: WidgetKinds.local.user,
title: user?.nip05 || user?.name || user?.display_name,
content: pubkey,
})

View File

@ -19,7 +19,7 @@ export function TitleBar({ id, title }: { id?: string; title: string }) {
<button
type="button"
onClick={() => remove(db, id)}
className="inline-flex h-6 w-6 shrink-0 transform items-center justify-center rounded backdrop-blur-xl hover:bg-white/10"
className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded backdrop-blur-xl hover:bg-white/10"
>
<CancelIcon className="h-3 w-3 text-white" />
</button>

View File

@ -10,10 +10,10 @@ import { TitleBar } from '@shared/titleBar';
import { Widget } from '@utils/types';
export function ArticleWidget({ params }: { params: Widget }) {
export function GlobalArticlesWidget({ params }: { params: Widget }) {
const { ndk } = useNDK();
const { status, data } = useQuery(
['article-widget', params.content],
['global-articles-widget'],
async () => {
const events = await ndk.fetchEvents({
kinds: [NDKKind.Article],

View File

@ -10,12 +10,13 @@ import { TitleBar } from '@shared/titleBar';
import { Widget } from '@utils/types';
export function FileWidget({ params }: { params: Widget }) {
export function GlobalFilesWidget({ params }: { params: Widget }) {
const { ndk } = useNDK();
const { status, data } = useQuery(
['file-widget', params.content],
['global-files-widget'],
async () => {
const events = await ndk.fetchEvents({
// @ts-expect-error, NDK not support file metadata yet
kinds: [1063],
limit: 100,
});

View File

@ -19,10 +19,10 @@ import { TitleBar } from '@shared/titleBar';
import { nHoursAgo } from '@utils/date';
import { Widget } from '@utils/types';
export function HashtagWidget({ params }: { params: Widget }) {
export function GlobalHashtagWidget({ params }: { params: Widget }) {
const { ndk } = useNDK();
const { status, data } = useQuery(
['hashtag-widget', params.content],
['global-hashtag-widget', params.content],
async () => {
const events = await ndk.fetchEvents({
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Article],

View File

@ -0,0 +1,13 @@
export * from './local/feeds';
export * from './local/network';
export * from './local/user';
export * from './local/thread';
export * from './local/files';
export * from './local/articles';
export * from './global/articles';
export * from './global/files';
export * from './global/hashtag';
export * from './nostrBand/trendingNotes';
export * from './nostrBand/trendingAccounts';
export * from './tmp/feeds';
export * from './tmp/hashtag';

View File

@ -0,0 +1,130 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { FileNote, NoteSkeleton, NoteWrapper } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { DBEvent, Widget } from '@utils/types';
export function LocalArticlesWidget({ params }: { params: Widget }) {
const { db } = useStorage();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['local-articles-widget'],
queryFn: async ({ pageParam = 0 }) => {
return await db.getAllEventsByKinds([NDKKind.Article], 20, pageParam);
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
const dbEvents = useMemo(
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
);
},
[data]
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white">
There have been no new posts.
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</div>
</div>
);
}

View File

@ -19,11 +19,11 @@ import { TitleBar } from '@shared/titleBar';
import { DBEvent, Widget } from '@utils/types';
export function FeedWidget({ params }: { params: Widget }) {
export function LocalFeedsWidget({ params }: { params: Widget }) {
const { db } = useStorage();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['groupfeed-widget', params.content],
queryKey: ['local-feeds-widget', params.content],
queryFn: async ({ pageParam = 0 }) => {
const authors = JSON.parse(params.content);
return await db.getAllEventsByAuthors(authors, 20, pageParam);

View File

@ -0,0 +1,130 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { FileNote, NoteSkeleton, NoteWrapper } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { DBEvent, Widget } from '@utils/types';
export function LocalFilesWidget({ params }: { params: Widget }) {
const { db } = useStorage();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['local-files-widget'],
queryFn: async ({ pageParam = 0 }) => {
return await db.getAllEventsByKinds([1063], 20, pageParam);
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
const dbEvents = useMemo(
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
);
},
[data]
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white">
There have been no new posts.
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</div>
</div>
);
}

View File

@ -21,12 +21,12 @@ import { useNostr } from '@utils/hooks/useNostr';
import { toRawEvent } from '@utils/rawEvent';
import { DBEvent } from '@utils/types';
export function NetworkWidget() {
export function LocalNetworkWidget() {
const { sub } = useNostr();
const { db } = useStorage();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['network-widget'],
queryKey: ['local-network-widget'],
queryFn: async ({ pageParam = 0 }) => {
return await db.getAllEvents(30, pageParam);
},

View File

@ -20,7 +20,7 @@ import { TitleBar } from '@shared/titleBar';
import { useEvent } from '@utils/hooks/useEvent';
import { Widget } from '@utils/types';
export function ThreadBlock({ params }: { params: Widget }) {
export function LocalThreadWidget({ params }: { params: Widget }) {
const { db } = useStorage();
const { status, data } = useEvent(params.content);

View File

@ -20,10 +20,10 @@ import { UserProfile } from '@shared/userProfile';
import { nHoursAgo } from '@utils/date';
import { Widget } from '@utils/types';
export function UserWidget({ params }: { params: Widget }) {
export function LocalUserWidget({ params }: { params: Widget }) {
const { ndk } = useNDK();
const { status, data } = useQuery(
['user-widget', params.content],
['local-user-widget', params.content],
async () => {
const events = await ndk.fetchEvents({
kinds: [1, 6],

View File

@ -1,9 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { type Profile, UserProfile } from '@app/space/components/userProfile';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { NostrBandUserProfile, type Profile } from '@shared/widgets/nostrBandUserProfile';
import { Widget } from '@utils/types';
@ -11,11 +10,11 @@ interface Response {
profiles: Array<{ pubkey: string }>;
}
export function TrendingProfilesWidget({ params }: { params: Widget }) {
export function TrendingAccountsWidget({ params }: { params: Widget }) {
const { status, data } = useQuery(
['trending-profiles-widget'],
async () => {
const res = await fetch(params.content);
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
if (!res.ok) {
throw new Error('Error');
}
@ -32,9 +31,9 @@ export function TrendingProfilesWidget({ params }: { params: Widget }) {
);
return (
<div className="scrollbar-hide relative shrink-0 grow-0 basis-[400px] overflow-y-auto bg-white/10 backdrop-blur-xl">
<TitleBar id={params.id} title={params.title} />
<div className="h-full">
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<TitleBar id={params.id} title="Trending Accounts" />
<div className="scrollbar-hide h-full max-w-full overflow-y-auto pb-20">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
@ -52,7 +51,7 @@ export function TrendingProfilesWidget({ params }: { params: Widget }) {
) : (
<div className="relative flex w-full flex-col gap-3 px-3 pt-1.5">
{data.map((item: Profile) => (
<UserProfile key={item.pubkey} data={item} />
<NostrBandUserProfile key={item.pubkey} data={item} />
))}
</div>
)}

View File

@ -14,7 +14,7 @@ export function TrendingNotesWidget({ params }: { params: Widget }) {
const { status, data } = useQuery(
['trending-notes-widget'],
async () => {
const res = await fetch(params.content);
const res = await fetch('https://api.nostr.band/v0/trending/notes');
if (!res.ok) {
throw new Error('failed to fecht trending notes');
}
@ -31,9 +31,9 @@ export function TrendingNotesWidget({ params }: { params: Widget }) {
);
return (
<div className="scrollbar-hide relative shrink-0 grow-0 basis-[400px] overflow-y-auto bg-white/10 backdrop-blur-xl">
<TitleBar id={params.id} title={params.title} />
<div className="h-full">
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<TitleBar id={params.id} title="Trending Notes" />
<div className="scrollbar-hide h-full max-w-full overflow-y-auto pb-20">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">

View File

@ -15,7 +15,7 @@ export interface Profile {
profile: { content: string };
}
export function UserProfile({ data }: { data: Profile }) {
export function NostrBandUserProfile({ data }: { data: Profile }) {
const { db } = useStorage();
const { addContact, removeContact } = useNostr();
const { status, data: userStats } = useQuery(

View File

@ -10,7 +10,7 @@ import { WidgetKinds, useWidgets } from '@stores/widgets';
import { Widget } from '@utils/types';
export function FeedWidgetForm({ params }: { params: Widget }) {
export function XfeedsWidget({ params }: { params: Widget }) {
const { db } = useStorage();
const [setWidget, removeWidget] = useWidgets((state) => [
@ -34,7 +34,7 @@ export function FeedWidgetForm({ params }: { params: Widget }) {
const submit = async () => {
setWidget(db, {
kind: WidgetKinds.feed,
kind: WidgetKinds.local.feeds,
title: title || 'Group',
content: JSON.stringify(groups),
});

View File

@ -26,7 +26,7 @@ const resolver: Resolver<FormValues> = async (values) => {
};
};
export function HashTagWidgetForm({ params }: { params: Widget }) {
export function XhashtagWidget({ params }: { params: Widget }) {
const [setWidget, removeWidget] = useWidgets((state) => [
state.setWidget,
state.removeWidget,
@ -47,7 +47,7 @@ export function HashTagWidgetForm({ params }: { params: Widget }) {
const onSubmit = async (data: FormValues) => {
try {
setWidget(db, {
kind: WidgetKinds.hashtag,
kind: WidgetKinds.global.hashtag,
title: data.hashtag + ' in 24 hours ago',
content: data.hashtag.replace('#', ''),
});

View File

@ -13,19 +13,81 @@ interface WidgetState {
}
export const WidgetKinds = {
feed: 1, // NIP-01
thread: 2, // NIP-01
hashtag: 3, // NIP-01
article: 4, // NIP-23
user: 5, // NIP-01
trendingProfiles: 6,
trendingNotes: 7,
file: 8, // NIP-94
network: 9999,
xfeed: 10000, // x is temporary state for new feed widget form
xhashtag: 10001, // x is temporary state for new hashtag widget form
local: {
network: 100,
feeds: 101,
files: 102,
articles: 103,
user: 104,
thread: 105,
},
global: {
feeds: 1000,
files: 1001,
articles: 1002,
hashtag: 1003,
},
nostrBand: {
trendingAccounts: 1,
trendingNotes: 2,
},
tmp: {
list: 10000,
xfeed: 10001,
xhashtag: 10002,
},
};
export const DefaultWidgets = [
{
title: 'Network / Follows',
data: [
{
kind: WidgetKinds.tmp.xfeed,
title: 'Group feeds',
},
{
kind: WidgetKinds.local.files,
title: 'Files',
},
{
kind: WidgetKinds.local.articles,
title: 'Articles',
},
],
},
{
title: 'Global',
data: [
{
kind: WidgetKinds.tmp.xhashtag,
title: 'Hashtag',
},
{
kind: WidgetKinds.global.files,
title: 'Files',
},
{
kind: WidgetKinds.global.articles,
title: 'Articles',
},
],
},
{
title: 'Trending (nostr.band)',
data: [
{
kind: WidgetKinds.nostrBand.trendingAccounts,
title: 'Accounts',
},
{
kind: WidgetKinds.nostrBand.trendingNotes,
title: 'Notes',
},
],
},
];
export const useWidgets = create<WidgetState>()(
persist(
(set) => ({
@ -39,7 +101,7 @@ export const useWidgets = create<WidgetState>()(
id: '9999',
title: 'Network',
content: '',
kind: WidgetKinds.network,
kind: WidgetKinds.local.network,
});
set({ widgets: dbWidgets });

10
src/utils/types.d.ts vendored
View File

@ -36,6 +36,16 @@ export interface Profile extends NDKUserProfile {
pubkey?: string;
}
export interface WidgetGroup {
title: string;
data: WidgetGroupItem[];
}
export interface WidgetGroupItem {
title: string;
kind: number;
}
export interface Widget {
id?: string;
account_id?: number;