wip: rework widget

This commit is contained in:
reya 2023-11-09 18:02:25 +07:00
parent cb9006abb2
commit 0cdf199cb5
31 changed files with 155 additions and 509 deletions

View File

@ -54,8 +54,8 @@ export default function App() {
{ {
path: '', path: '',
async lazy() { async lazy() {
const { SpaceScreen } = await import('@app/space'); const { HomeScreen } = await import('@app/home');
return { Component: SpaceScreen }; return { Component: HomeScreen };
}, },
}, },
{ {

View File

@ -2,16 +2,12 @@ import { message } from '@tauri-apps/plugin-dialog';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import { ArrowLeftIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowLeftIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { HASHTAGS, WIDGET_KIND } from '@stores/constants'; import { HASHTAGS } from '@stores/constants';
import { useOnboarding } from '@stores/onboarding'; import { useOnboarding } from '@stores/onboarding';
export function OnboardHashtagScreen() { export function OnboardHashtagScreen() {
const { db } = useStorage();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [tags, setTags] = useState(new Set<string>()); const [tags, setTags] = useState(new Set<string>());
@ -34,10 +30,6 @@ export function OnboardHashtagScreen() {
try { try {
setLoading(true); setLoading(true);
for (const tag of tags) {
await db.createWidget(WIDGET_KIND.global.hashtag, tag, tag.replace('#', ''));
}
setHashtag(); setHashtag();
navigate(-1); navigate(-1);
} catch (e) { } catch (e) {

View File

@ -2,34 +2,21 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from 'virtua';
import { ToggleWidgetList } from '@app/space/components/toggle';
import { WidgetList } from '@app/space/components/widgetList';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { import {
GlobalArticlesWidget,
GlobalFilesWidget,
GlobalHashtagWidget,
LocalArticlesWidget,
LocalFeedsWidget,
LocalFilesWidget,
LocalThreadWidget,
LocalUserWidget,
NewsfeedWidget, NewsfeedWidget,
NotificationWidget, NotificationWidget,
TrendingAccountsWidget, ToggleWidgetList,
TrendingNotesWidget, WidgetList,
XfeedsWidget,
XhashtagWidget,
} from '@shared/widgets'; } from '@shared/widgets';
import { WIDGET_KIND } from '@stores/constants'; import { WIDGET_KIND } from '@stores/constants';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function SpaceScreen() { export function HomeScreen() {
const ref = useRef<VListHandle>(null); const ref = useRef<VListHandle>(null);
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(-1);
@ -40,16 +27,16 @@ export function SpaceScreen() {
const dbWidgets = await db.getWidgets(); const dbWidgets = await db.getWidgets();
const defaultWidgets = [ const defaultWidgets = [
{ {
id: '9998', id: '99999',
title: 'Notification',
content: '',
kind: WIDGET_KIND.local.notification,
},
{
id: '9999',
title: 'Newsfeed', title: 'Newsfeed',
content: '', content: '',
kind: WIDGET_KIND.local.network, kind: WIDGET_KIND.newsfeed,
},
{
id: '99998',
title: 'Notification',
content: '',
kind: WIDGET_KIND.notification,
}, },
]; ];
@ -63,36 +50,12 @@ export function SpaceScreen() {
const renderItem = useCallback((widget: Widget) => { const renderItem = useCallback((widget: Widget) => {
switch (widget.kind) { switch (widget.kind) {
case WIDGET_KIND.local.feeds: case WIDGET_KIND.notification:
return <LocalFeedsWidget key={widget.id} params={widget} />;
case WIDGET_KIND.local.files:
return <LocalFilesWidget key={widget.id} params={widget} />;
case WIDGET_KIND.local.articles:
return <LocalArticlesWidget key={widget.id} params={widget} />;
case WIDGET_KIND.local.user:
return <LocalUserWidget key={widget.id} params={widget} />;
case WIDGET_KIND.local.thread:
return <LocalThreadWidget key={widget.id} params={widget} />;
case WIDGET_KIND.global.hashtag:
return <GlobalHashtagWidget key={widget.id} params={widget} />;
case WIDGET_KIND.global.articles:
return <GlobalArticlesWidget key={widget.id} params={widget} />;
case WIDGET_KIND.global.files:
return <GlobalFilesWidget key={widget.id} params={widget} />;
case WIDGET_KIND.nostrBand.trendingAccounts:
return <TrendingAccountsWidget key={widget.id} params={widget} />;
case WIDGET_KIND.nostrBand.trendingNotes:
return <TrendingNotesWidget key={widget.id} params={widget} />;
case WIDGET_KIND.tmp.xfeed:
return <XfeedsWidget key={widget.id} params={widget} />;
case WIDGET_KIND.tmp.xhashtag:
return <XhashtagWidget key={widget.id} params={widget} />;
case WIDGET_KIND.tmp.list:
return <WidgetList key={widget.id} params={widget} />;
case WIDGET_KIND.local.notification:
return <NotificationWidget key={widget.id} />; return <NotificationWidget key={widget.id} />;
case WIDGET_KIND.local.network: case WIDGET_KIND.newsfeed:
return <NewsfeedWidget key={widget.id} />; return <NewsfeedWidget key={widget.id} />;
case WIDGET_KIND.list:
return <WidgetList key={widget.id} widget={widget} />;
default: default:
return null; return null;
} }
@ -108,14 +71,15 @@ export function SpaceScreen() {
return ( return (
<VList <VList
className="h-full w-full flex-nowrap overflow-x-auto !overflow-y-hidden scrollbar-none focus:outline-none"
horizontal
ref={ref} ref={ref}
className="h-full w-full flex-nowrap overflow-x-auto !overflow-y-hidden scrollbar-none focus:outline-none"
initialItemSize={420} initialItemSize={420}
tabIndex={0} tabIndex={0}
horizontal
onKeyDown={(e) => { onKeyDown={(e) => {
if (!ref.current) return; if (!ref.current) return;
switch (e.code) { switch (e.code) {
case 'ArrowUp':
case 'ArrowLeft': { case 'ArrowLeft': {
e.preventDefault(); e.preventDefault();
const prevIndex = Math.max(selectedIndex - 1, 0); const prevIndex = Math.max(selectedIndex - 1, 0);
@ -126,6 +90,7 @@ export function SpaceScreen() {
}); });
break; break;
} }
case 'ArrowDown':
case 'ArrowRight': { case 'ArrowRight': {
e.preventDefault(); e.preventDefault();
const nextIndex = Math.min(selectedIndex + 1, data.length - 1); const nextIndex = Math.min(selectedIndex + 1, data.length - 1);
@ -136,6 +101,8 @@ export function SpaceScreen() {
}); });
break; break;
} }
default:
break;
} }
}} }}
> >

View File

@ -1,128 +0,0 @@
import { useCallback } from 'react';
import {
ArticleIcon,
BellIcon,
FileIcon,
FollowsIcon,
GroupFeedsIcon,
HashtagIcon,
ThreadsIcon,
TrendingIcon,
} from '@shared/icons';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { DEFAULT_WIDGETS, WIDGET_KIND } from '@stores/constants';
import { useWidget } from '@utils/hooks/useWidget';
import { Widget, WidgetGroup, WidgetGroupItem } from '@utils/types';
export function WidgetList({ params }: { params: Widget }) {
const { addWidget, removeWidget } = useWidget();
const open = (item: WidgetGroupItem) => {
addWidget.mutate({
kind: item.kind,
title: item.title,
content: JSON.stringify(item.content),
});
removeWidget.mutate(params.id);
};
const renderIcon = useCallback((kind: number) => {
switch (kind) {
case WIDGET_KIND.tmp.xfeed:
return (
<GroupFeedsIcon className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />
);
case WIDGET_KIND.local.follows:
return <FollowsIcon className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />;
case WIDGET_KIND.local.files:
case WIDGET_KIND.global.files:
return <FileIcon className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />;
case WIDGET_KIND.local.articles:
case WIDGET_KIND.global.articles:
return <ArticleIcon className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />;
case WIDGET_KIND.tmp.xhashtag:
return <HashtagIcon className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />;
case WIDGET_KIND.nostrBand.trendingAccounts:
case WIDGET_KIND.nostrBand.trendingNotes:
return (
<TrendingIcon className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />
);
case WIDGET_KIND.local.notification:
return <BellIcon className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />;
case WIDGET_KIND.other.learnNostr:
return <ThreadsIcon className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />;
default:
return null;
}
}, []);
const renderItem = useCallback((row: WidgetGroup, index: number) => {
return (
<div key={index} className="flex flex-col gap-2">
<h3 className="font-semibold">{row.title}</h3>
<div className="flex flex-col divide-y divide-neutral-200 overflow-hidden rounded-xl bg-neutral-100 dark:divide-neutral-800 dark:bg-neutral-900">
{row.data.map((item, index) => (
<button
onClick={() => open(item)}
key={index}
className="group flex items-center gap-2.5 px-4 hover:bg-neutral-200 dark:hover:bg-neutral-800"
>
{item.icon ? (
<div className="h-10 w-10 shrink-0 rounded-lg">
<img
src={item.icon}
alt={item.title}
className="h-10 w-10 object-cover"
/>
</div>
) : (
<div className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-neutral-200 group-hover:bg-neutral-300 dark:bg-neutral-800 dark:group-hover:bg-neutral-700">
{renderIcon(item.kind)}
</div>
)}
<div className="inline-flex h-16 w-full flex-col items-start justify-center">
<h5 className="line-clamp-1 text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{item.title}
</h5>
<p className="line-clamp-1 text-sm text-neutral-600 dark:text-neutral-400">
{item.description}
</p>
</div>
</button>
))}
</div>
</div>
);
}, []);
return (
<WidgetWrapper>
<TitleBar id={params.id} title="Add widget" />
<div className="flex-1 overflow-y-auto pb-10 scrollbar-none">
<div className="flex flex-col gap-6 px-3">
{DEFAULT_WIDGETS.map((row: WidgetGroup, index: number) =>
renderItem(row, index)
)}
<div className="border-t border-neutral-200 pt-6 dark:border-neutral-800">
<button
type="button"
disabled
className="inline-flex h-14 w-full items-center justify-center gap-2.5 rounded-xl bg-neutral-50 text-sm font-medium text-neutral-900 dark:bg-neutral-950 dark:text-neutral-100"
>
Build your own widget{' '}
<div className="-rotate-3 transform-gpu rounded-md border border-neutral-200 bg-neutral-100 px-1.5 py-1 dark:border-neutral-800 dark:bg-neutral-900">
<span className="bg-gradient-to-r from-blue-400 via-red-400 to-orange-500 bg-clip-text text-xs text-transparent dark:from-blue-200 dark:via-red-200 dark:to-orange-300">
Coming soon
</span>
</div>
</button>
</div>
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -34,7 +34,7 @@ export function NoteActions({
type="button" type="button"
onClick={() => onClick={() =>
addWidget.mutate({ addWidget.mutate({
kind: WIDGET_KIND.local.thread, kind: WIDGET_KIND.thread,
title: 'Thread', title: 'Thread',
content: id, content: id,
}) })

View File

@ -10,7 +10,7 @@ export function Hashtag({ tag }: { tag: string }) {
type="button" type="button"
onClick={() => onClick={() =>
addWidget.mutate({ addWidget.mutate({
kind: WIDGET_KIND.global.hashtag, kind: WIDGET_KIND.hashtag,
title: tag, title: tag,
content: tag.replace('#', ''), content: tag.replace('#', ''),
}) })

View File

@ -50,7 +50,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
type="button" type="button"
onClick={() => onClick={() =>
addWidget.mutate({ addWidget.mutate({
kind: WIDGET_KIND.local.thread, kind: WIDGET_KIND.thread,
title: 'Thread', title: 'Thread',
content: data.id, content: data.id,
}) })

View File

@ -14,14 +14,7 @@ export const MentionUser = memo(function MentionUser({ pubkey }: { pubkey: strin
type="button" type="button"
onClick={() => onClick={() =>
addWidget.mutate({ addWidget.mutate({
kind: WIDGET_KIND.local.user, kind: WIDGET_KIND.user,
title: user?.name || user?.display_name || user?.displayName,
content: pubkey,
})
}
onKeyDown={() =>
addWidget.mutate({
kind: WIDGET_KIND.local.user,
title: user?.name || user?.display_name || user?.displayName, title: user?.name || user?.display_name || user?.displayName,
content: pubkey, content: pubkey,
}) })

View File

@ -91,7 +91,7 @@ export function NotifyNote({ event }: { event: NDKEvent }) {
type="button" type="button"
onClick={() => onClick={() =>
addWidget.mutate({ addWidget.mutate({
kind: WIDGET_KIND.local.thread, kind: WIDGET_KIND.thread,
title: 'Thread', title: 'Thread',
content: data.id, content: data.id,
}) })

View File

@ -30,7 +30,7 @@ export function TextNote({ event }: { event: NDKEvent }) {
type="button" type="button"
onClick={() => onClick={() =>
addWidget.mutate({ addWidget.mutate({
kind: WIDGET_KIND.local.thread, kind: WIDGET_KIND.thread,
title: 'Thread', title: 'Thread',
content: thread.rootEventId, content: thread.rootEventId,
}) })

View File

@ -1,5 +1,6 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { FetchFilter } from 'nostr-fetch';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
@ -15,12 +16,12 @@ import { FETCH_LIMIT } from '@stores/constants';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function LocalArticlesWidget({ params }: { params: Widget }) { export function ArticleWidget({ widget }: { widget: Widget }) {
const { db } = useStorage(); const { db } = useStorage();
const { ndk, relayUrls, fetcher } = useNDK(); const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['local-articles'], queryKey: ['widget-' + widget.id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,
@ -29,15 +30,24 @@ export function LocalArticlesWidget({ params }: { params: Widget }) {
signal: AbortSignal; signal: AbortSignal;
pageParam: number; pageParam: number;
}) => { }) => {
const events = await fetcher.fetchLatestEvents( let filter: FetchFilter;
relayUrls, const content = JSON.parse(widget.content);
{
if (content.global) {
filter = {
kinds: [NDKKind.Article],
};
} else {
filter = {
kinds: [NDKKind.Article], kinds: [NDKKind.Article],
authors: db.account.circles, authors: db.account.circles,
}, };
FETCH_LIMIT, }
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
); const events = await fetcher.fetchLatestEvents(relayUrls, filter, FETCH_LIMIT, {
asOf: pageParam === 0 ? undefined : pageParam,
abortSignal: signal,
});
const ndkEvents = events.map((event) => { const ndkEvents = events.map((event) => {
return new NDKEvent(ndk, event); return new NDKEvent(ndk, event);
@ -61,7 +71,7 @@ export function LocalArticlesWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <TitleBar id={widget.id} title={widget.title} />
<VList className="flex-1"> <VList className="flex-1">
{status === 'pending' ? ( {status === 'pending' ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">

View File

@ -1,5 +1,6 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { FetchFilter } from 'nostr-fetch';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
@ -15,12 +16,12 @@ import { FETCH_LIMIT } from '@stores/constants';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function LocalFilesWidget({ params }: { params: Widget }) { export function FileWidget({ widget }: { widget: Widget }) {
const { db } = useStorage(); const { db } = useStorage();
const { ndk, relayUrls, fetcher } = useNDK(); const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['local-files'], queryKey: ['widget-' + widget.id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,
@ -29,15 +30,24 @@ export function LocalFilesWidget({ params }: { params: Widget }) {
signal: AbortSignal; signal: AbortSignal;
pageParam: number; pageParam: number;
}) => { }) => {
const events = await fetcher.fetchLatestEvents( let filter: FetchFilter;
relayUrls, const content = JSON.parse(widget.content);
{
if (content.global) {
filter = {
kinds: [1063],
};
} else {
filter = {
kinds: [1063], kinds: [1063],
authors: db.account.circles, authors: db.account.circles,
}, };
FETCH_LIMIT, }
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
); const events = await fetcher.fetchLatestEvents(relayUrls, filter, FETCH_LIMIT, {
asOf: pageParam === 0 ? undefined : pageParam,
abortSignal: signal,
});
const ndkEvents = events.map((event) => { const ndkEvents = events.map((event) => {
return new NDKEvent(ndk, event); return new NDKEvent(ndk, event);
@ -61,7 +71,7 @@ export function LocalFilesWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <TitleBar id={widget.id} title={widget.title} />
<VList className="flex-1"> <VList className="flex-1">
{status === 'pending' ? ( {status === 'pending' ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">

View File

@ -1,106 +0,0 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedArticleNote } 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 GlobalArticlesWidget({ params }: { params: Widget }) {
const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['global-articles'],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await fetcher.fetchLatestEvents(
relayUrls,
{
kinds: [NDKKind.Article],
},
FETCH_LIMIT,
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
);
const ndkEvents = events.map((event) => {
return new NDKEvent(ndk, event);
});
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;
},
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data]
);
return (
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<VList className="flex-1">
{status === 'pending' ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-5 w-5 animate-spin" />
</div>
) : allEvents.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="font-semibold leading-tight text-neutral-900 dark:text-neutral-100">
Oops, it looks like there are no articles.
</h3>
<p className="text-neutral-500 dark:text-neutral-400">
You can close this widget
</p>
</div>
</div>
</div>
) : (
allEvents.map((item) => <MemoizedArticleNote key={item.id} event={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>
);
}

View File

@ -1,106 +0,0 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedFileNote } 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 GlobalFilesWidget({ params }: { params: Widget }) {
const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['global-files'],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await fetcher.fetchLatestEvents(
relayUrls,
{
kinds: [1063],
},
FETCH_LIMIT,
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
);
const ndkEvents = events.map((event) => {
return new NDKEvent(ndk, event);
});
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;
},
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data]
);
return (
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<VList className="flex-1">
{status === 'pending' ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-5 w-5 animate-spin" />
</div>
) : allEvents.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="font-semibold leading-tight text-neutral-900 dark:text-neutral-100">
Oops, it looks like there are no files.
</h3>
<p className="text-neutral-500 dark:text-neutral-400">
You can close this widget
</p>
</div>
</div>
</div>
) : (
allEvents.map((item) => <MemoizedFileNote key={item.id} event={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>
);
}

View File

@ -19,11 +19,11 @@ import { FETCH_LIMIT } from '@stores/constants';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function LocalFeedsWidget({ params }: { params: Widget }) { export function GroupWidget({ widget }: { widget: Widget }) {
const { relayUrls, ndk, fetcher } = useNDK(); const { relayUrls, ndk, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['group-feeds-' + params.id], queryKey: ['widget-' + widget.id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,
@ -32,11 +32,12 @@ export function LocalFeedsWidget({ params }: { params: Widget }) {
signal: AbortSignal; signal: AbortSignal;
pageParam: number; pageParam: number;
}) => { }) => {
const authors = JSON.parse(widget.content);
const events = await fetcher.fetchLatestEvents( const events = await fetcher.fetchLatestEvents(
relayUrls, relayUrls,
{ {
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
authors: JSON.parse(params.content), authors: authors,
}, },
FETCH_LIMIT, FETCH_LIMIT,
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } { asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
@ -78,7 +79,7 @@ export function LocalFeedsWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <TitleBar id={widget.id} title={widget.title} />
<VList className="flex-1"> <VList className="flex-1">
{status === 'pending' ? ( {status === 'pending' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">

View File

@ -14,11 +14,11 @@ import { FETCH_LIMIT } from '@stores/constants';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function GlobalHashtagWidget({ params }: { params: Widget }) { export function HashtagWidget({ widget }: { widget: Widget }) {
const { ndk, relayUrls, fetcher } = useNDK(); const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['hashtag-' + params.title], queryKey: ['widget-' + widget.id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,
@ -31,7 +31,7 @@ export function GlobalHashtagWidget({ params }: { params: Widget }) {
relayUrls, relayUrls,
{ {
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
'#t': [params.content], '#t': [widget.content],
}, },
FETCH_LIMIT, FETCH_LIMIT,
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } { asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
@ -74,7 +74,7 @@ export function GlobalHashtagWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <TitleBar id={widget.id} title={widget.title} />
<VList className="flex-1"> <VList className="flex-1">
{status === 'pending' ? ( {status === 'pending' ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
@ -86,7 +86,7 @@ export function GlobalHashtagWidget({ params }: { params: Widget }) {
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" /> <img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center"> <div className="text-center">
<h3 className="font-semibold leading-tight text-neutral-900 dark:text-neutral-100"> <h3 className="font-semibold leading-tight text-neutral-900 dark:text-neutral-100">
Oops, it looks like there are no events related to {params.title}. Oops, it looks like there are no events related to {widget.content}.
</h3> </h3>
<p className="text-neutral-500 dark:text-neutral-400"> <p className="text-neutral-500 dark:text-neutral-400">
You can close this widget You can close this widget

View File

@ -1,17 +1,15 @@
export * from './wrapper';
export * from './local/feeds';
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';
export * from './newsfeed'; export * from './newsfeed';
export * from './notification'; export * from './notification';
export * from './liveUpdater';
export * from './topic'; export * from './topic';
export * from './user';
export * from './article';
export * from './file';
export * from './hashtag';
export * from './thread';
export * from './group';
export * from './nostrBand/trendingAccounts';
export * from './nostrBand/trendingNotes';
export * from './other/wrapper';
export * from './other/liveUpdater';
export * from './other/toggleWidgetList';
export * from './other/widgetList';

View File

@ -14,8 +14,7 @@ import {
UnknownNote, UnknownNote,
} from '@shared/notes'; } from '@shared/notes';
import { TitleBar } from '@shared/titleBar'; import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets'; import { LiveUpdater, WidgetWrapper } from '@shared/widgets';
import { LiveUpdater } from '@shared/widgets';
import { FETCH_LIMIT } from '@stores/constants'; import { FETCH_LIMIT } from '@stores/constants';

View File

@ -4,7 +4,10 @@ import { VList } from 'virtua';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { TitleBar } from '@shared/titleBar'; import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets'; import { WidgetWrapper } from '@shared/widgets';
import { NostrBandUserProfile, type Profile } from '@shared/widgets/nostrBandUserProfile'; import {
NostrBandUserProfile,
type Profile,
} from '@shared/widgets/other/nostrBandUserProfile';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';

View File

@ -13,9 +13,9 @@ interface Response {
notes: Array<{ event: NDKEvent }>; notes: Array<{ event: NDKEvent }>;
} }
export function TrendingNotesWidget({ params }: { params: Widget }) { export function TrendingNotesWidget({ widget }: { widget: Widget }) {
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['trending-notes-widget'], queryKey: ['widget-' + widget.id],
queryFn: async () => { queryFn: async () => {
const res = await fetch('https://api.nostr.band/v0/trending/notes'); const res = await fetch('https://api.nostr.band/v0/trending/notes');
if (!res.ok) { if (!res.ok) {
@ -33,7 +33,7 @@ export function TrendingNotesWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title="Trending Notes" /> <TitleBar id={widget.id} title="Trending Notes" />
<VList className="flex-1"> <VList className="flex-1">
{status === 'pending' ? ( {status === 'pending' ? (
<div className="flex h-full w-full items-center justify-center "> <div className="flex h-full w-full items-center justify-center ">

View File

@ -14,7 +14,7 @@ export function ToggleWidgetList() {
<button <button
type="button" type="button"
onClick={() => onClick={() =>
addWidget.mutate({ kind: WIDGET_KIND.tmp.list, title: '', content: '' }) addWidget.mutate({ kind: WIDGET_KIND.list, title: '', content: '' })
} }
className="inline-flex h-9 items-center gap-2 rounded-full bg-neutral-200 px-3 text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700" className="inline-flex h-9 items-center gap-2 rounded-full bg-neutral-200 px-3 text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
> >

View File

@ -0,0 +1,30 @@
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { Widget } from '@utils/types';
export function WidgetList({ widget }: { widget: Widget }) {
return (
<WidgetWrapper>
<TitleBar id={widget.id} title="Add widgets" />
<div className="flex-1 overflow-y-auto pb-10 scrollbar-none">
<div className="flex flex-col gap-6 px-3">
<div className="border-t border-neutral-200 pt-6 dark:border-neutral-800">
<button
type="button"
disabled
className="inline-flex h-14 w-full items-center justify-center gap-2.5 rounded-xl bg-neutral-50 text-sm font-medium text-neutral-900 dark:bg-neutral-950 dark:text-neutral-100"
>
Build your own widget{' '}
<div className="-rotate-3 transform-gpu rounded-md border border-neutral-200 bg-neutral-100 px-1.5 py-1 dark:border-neutral-800 dark:bg-neutral-900">
<span className="bg-gradient-to-r from-blue-400 via-red-400 to-orange-500 bg-clip-text text-xs text-transparent dark:from-blue-200 dark:via-red-200 dark:to-orange-300">
Coming soon
</span>
</div>
</button>
</div>
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -18,8 +18,8 @@ import { WidgetWrapper } from '@shared/widgets';
import { useEvent } from '@utils/hooks/useEvent'; import { useEvent } from '@utils/hooks/useEvent';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function LocalThreadWidget({ params }: { params: Widget }) { export function ThreadWidget({ widget }: { widget: Widget }) {
const { status, data } = useEvent(params.content); const { status, data } = useEvent(widget.content);
const renderKind = useCallback( const renderKind = useCallback(
(event: NDKEvent) => { (event: NDKEvent) => {
@ -39,7 +39,7 @@ export function LocalThreadWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <TitleBar id={widget.id} title={widget.title} />
<WVList className="flex-1 overflow-y-auto px-3 pb-5"> <WVList className="flex-1 overflow-y-auto px-3 pb-5">
{status === 'pending' ? ( {status === 'pending' ? (
<div className="flex h-16 items-center justify-center rounded-xl bg-neutral-50 px-3 py-3 dark:bg-neutral-950"> <div className="flex h-16 items-center justify-center rounded-xl bg-neutral-50 px-3 py-3 dark:bg-neutral-950">
@ -52,7 +52,7 @@ export function LocalThreadWidget({ params }: { params: Widget }) {
{renderKind(data)} {renderKind(data)}
<NoteActions id={data.id} pubkey={data.pubkey} /> <NoteActions id={data.id} pubkey={data.pubkey} />
</div> </div>
<NoteReplyForm eventId={params.content} /> <NoteReplyForm eventId={widget.content} />
<ReplyList eventId={data.id} /> <ReplyList eventId={data.id} />
</> </>
)} )}

View File

@ -28,7 +28,7 @@ export function XfeedsWidget({ params }: { params: Widget }) {
const submit = async () => { const submit = async () => {
addWidget.mutate({ addWidget.mutate({
kind: WIDGET_KIND.local.feeds, kind: WIDGET_KIND.group,
title: title || 'Group', title: title || 'Group',
content: JSON.stringify(groups), content: JSON.stringify(groups),
}); });

View File

@ -39,7 +39,7 @@ export function XhashtagWidget({ params }: { params: Widget }) {
const onSubmit = async (data: FormValues) => { const onSubmit = async (data: FormValues) => {
try { try {
addWidget.mutate({ addWidget.mutate({
kind: WIDGET_KIND.global.hashtag, kind: WIDGET_KIND.hashtag,
title: data.hashtag, title: data.hashtag,
content: data.hashtag.replace('#', ''), content: data.hashtag.replace('#', ''),
}); });

View File

@ -23,7 +23,7 @@ export function TopicWidget({ widget }: { widget: Widget }) {
const { relayUrls, ndk, fetcher } = useNDK(); const { relayUrls, ndk, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: [widget.title], queryKey: ['widget-' + widget.id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,

View File

@ -18,17 +18,17 @@ import { WidgetWrapper } from '@shared/widgets';
import { nHoursAgo } from '@utils/date'; import { nHoursAgo } from '@utils/date';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function LocalUserWidget({ params }: { params: Widget }) { export function UserWidget({ widget }: { widget: Widget }) {
const { ndk } = useNDK(); const { ndk } = useNDK();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['user-posts', params.content], queryKey: ['widget-' + widget.id],
queryFn: async () => { queryFn: async () => {
const rootIds = new Set(); const rootIds = new Set();
const dedupQueue = new Set(); const dedupQueue = new Set();
const events = await ndk.fetchEvents({ const events = await ndk.fetchEvents({
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
authors: [params.content], authors: [widget.content],
since: nHoursAgo(24), since: nHoursAgo(24),
}); });
@ -70,10 +70,10 @@ export function LocalUserWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <TitleBar id={widget.id} title={widget.title} />
<WVList className="flex-1 overflow-y-auto"> <WVList className="flex-1 overflow-y-auto">
<div className="px-3 pt-1.5"> <div className="px-3 pt-1.5">
<UserProfile pubkey={params.content} /> <UserProfile pubkey={widget.content} />
</div> </div>
<div> <div>
<h3 className="mb-3 mt-4 px-3 text-lg font-semibold text-neutral-900 dark:text-neutral-100"> <h3 className="mb-3 mt-4 px-3 text-lg font-semibold text-neutral-900 dark:text-neutral-100">

View File

@ -42,35 +42,18 @@ export const HASHTAGS = [
]; ];
export const WIDGET_KIND = { export const WIDGET_KIND = {
local: { user: 1,
network: 100, thread: 2,
feeds: 101, group: 3,
files: 102, article: 4,
articles: 103, file: 5,
user: 104, trendingNotes: 6,
thread: 105, trendingAccounts: 7,
follows: 106, topic: 8,
notification: 107, hashtag: 9,
}, notification: 9998,
global: { newsfeed: 9999,
feeds: 1000, list: 10000,
files: 1001,
articles: 1002,
hashtag: 1003,
topic: 108,
},
nostrBand: {
trendingAccounts: 1,
trendingNotes: 2,
},
other: {
learnNostr: 90000,
},
tmp: {
list: 10000,
xfeed: 10001,
xhashtag: 10002,
},
}; };
export const TOPICS = [ export const TOPICS = [