refactor(widget): migrate widget component to ark lib

This commit is contained in:
reya 2023-12-18 13:39:03 +07:00
parent 344bdc0c66
commit 55298515af
36 changed files with 1153 additions and 1016 deletions

View File

@ -22,6 +22,7 @@
"@getalby/sdk": "^2.7.0", "@getalby/sdk": "^2.7.0",
"@nostr-dev-kit/ndk": "^2.3.0", "@nostr-dev-kit/ndk": "^2.3.0",
"@nostr-fetch/adapter-ndk": "^0.14.1", "@nostr-fetch/adapter-ndk": "^0.14.1",
"@preact/signals-react": "^1.3.8",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",

View File

@ -17,6 +17,9 @@ dependencies:
'@nostr-fetch/adapter-ndk': '@nostr-fetch/adapter-ndk':
specifier: ^0.14.1 specifier: ^0.14.1
version: 0.14.1(@nostr-dev-kit/ndk@2.3.0)(nostr-fetch@0.14.1) version: 0.14.1(@nostr-dev-kit/ndk@2.3.0)(nostr-fetch@0.14.1)
'@preact/signals-react':
specifier: ^1.3.8
version: 1.3.8(react@18.2.0)
'@radix-ui/react-accordion': '@radix-ui/react-accordion':
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) version: 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0)
@ -876,6 +879,20 @@ packages:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false dev: false
/@preact/signals-core@1.5.1:
resolution: {integrity: sha512-dE6f+WCX5ZUDwXzUIWNMhhglmuLpqJhuy3X3xHrhZYI0Hm2LyQwOu0l9mdPiWrVNsE+Q7txOnJPgtIqHCYoBVA==}
dev: false
/@preact/signals-react@1.3.8(react@18.2.0):
resolution: {integrity: sha512-i7mVZ/ZiD9WqNH79r+klpQsp8X+/dOd/5AtvDI0HNpgWuHyzyF9WXDViKl+1vXgB767n9VnH1W2azg+w1oyFMQ==}
peerDependencies:
react: ^16.14.0 || 17.x || 18.x
dependencies:
'@preact/signals-core': 1.5.1
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/@radix-ui/primitive@1.0.1: /@radix-ui/primitive@1.0.1:
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
dependencies: dependencies:
@ -5981,6 +5998,14 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: false dev: false
/use-sync-external-store@1.2.0(react@18.2.0):
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/utf-8-validate@5.0.10: /utf-8-validate@5.0.10:
resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
engines: {node: '>=6.14.2'} engines: {node: '>=6.14.2'}

View File

@ -124,9 +124,11 @@ export default function App() {
}, },
], ],
}, },
],
},
{ {
path: 'auth', path: 'auth',
element: <AuthLayout />, element: <AuthLayout platform={ark.platform} />,
errorElement: <ErrorScreen />, errorElement: <ErrorScreen />,
children: [ children: [
{ {
@ -181,27 +183,21 @@ export default function App() {
{ {
path: 'tutorials/widget', path: 'tutorials/widget',
async lazy() { async lazy() {
const { TutorialWidgetScreen } = await import( const { TutorialWidgetScreen } = await import('@app/auth/tutorials/widget');
'@app/auth/tutorials/widget'
);
return { Component: TutorialWidgetScreen }; return { Component: TutorialWidgetScreen };
}, },
}, },
{ {
path: 'tutorials/posting', path: 'tutorials/posting',
async lazy() { async lazy() {
const { TutorialPostingScreen } = await import( const { TutorialPostingScreen } = await import('@app/auth/tutorials/posting');
'@app/auth/tutorials/posting'
);
return { Component: TutorialPostingScreen }; return { Component: TutorialPostingScreen };
}, },
}, },
{ {
path: 'tutorials/finish', path: 'tutorials/finish',
async lazy() { async lazy() {
const { TutorialFinishScreen } = await import( const { TutorialFinishScreen } = await import('@app/auth/tutorials/finish');
'@app/auth/tutorials/finish'
);
return { Component: TutorialFinishScreen }; return { Component: TutorialFinishScreen };
}, },
}, },
@ -209,7 +205,7 @@ export default function App() {
}, },
{ {
path: 'settings', path: 'settings',
element: <SettingsLayout />, element: <SettingsLayout platform={ark.platform} />,
errorElement: <ErrorScreen />, errorElement: <ErrorScreen />,
children: [ children: [
{ {
@ -263,8 +259,6 @@ export default function App() {
}, },
], ],
}, },
],
},
]); ]);
return ( return (

View File

@ -1,5 +1,6 @@
import { useSignal } from '@preact/signals-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback, useRef, useState } from 'react'; import { useRef } from 'react';
import { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from 'virtua';
import { useArk } from '@libs/ark'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
@ -22,27 +23,27 @@ import { WIDGET_KIND } from '@utils/constants';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function HomeScreen() { export function HomeScreen() {
const ref = useRef<VListHandle>(null);
const [selectedIndex, setSelectedIndex] = useState(-1);
const ark = useArk(); const ark = useArk();
const { status, data } = useQuery({ const ref = useRef<VListHandle>(null);
const index = useSignal(-1);
const { isLoading, data } = useQuery({
queryKey: ['widgets'], queryKey: ['widgets'],
queryFn: async () => { queryFn: async () => {
const dbWidgets = await ark.getWidgets(); const dbWidgets = await ark.getWidgets();
const defaultWidgets = [ const defaultWidgets = [
{
id: '9999',
title: 'Newsfeed',
content: '',
kind: WIDGET_KIND.newsfeed,
},
{ {
id: '9998', id: '9998',
title: 'Notification', title: 'Notification',
content: '', content: '',
kind: WIDGET_KIND.notification, kind: WIDGET_KIND.notification,
}, },
{
id: '9999',
title: 'Newsfeed',
content: '',
kind: WIDGET_KIND.newsfeed,
},
]; ];
return [...defaultWidgets, ...dbWidgets]; return [...defaultWidgets, ...dbWidgets];
@ -53,7 +54,7 @@ export function HomeScreen() {
staleTime: Infinity, staleTime: Infinity,
}); });
const renderItem = useCallback((widget: Widget) => { const renderItem = (widget: Widget) => {
switch (widget.kind) { switch (widget.kind) {
case WIDGET_KIND.notification: case WIDGET_KIND.notification:
return <NotificationWidget key={widget.id} />; return <NotificationWidget key={widget.id} />;
@ -80,13 +81,13 @@ export function HomeScreen() {
case WIDGET_KIND.list: case WIDGET_KIND.list:
return <WidgetList key={widget.id} widget={widget} />; return <WidgetList key={widget.id} widget={widget} />;
default: default:
return null; return <NewsfeedWidget key={widget.id} />;
} }
}, []); };
if (status === 'pending') { if (isLoading) {
return ( return (
<div className="flex h-full w-full items-center justify-center bg-white dark:bg-black"> <div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-5 w-5 animate-spin" /> <LoaderIcon className="h-5 w-5 animate-spin" />
</div> </div>
); );
@ -106,8 +107,8 @@ export function HomeScreen() {
case 'ArrowUp': case 'ArrowUp':
case 'ArrowLeft': { case 'ArrowLeft': {
e.preventDefault(); e.preventDefault();
const prevIndex = Math.max(selectedIndex - 1, 0); const prevIndex = Math.max(index.peek() - 1, 0);
setSelectedIndex(prevIndex); index.value = prevIndex;
ref.current.scrollToIndex(prevIndex, { ref.current.scrollToIndex(prevIndex, {
align: 'center', align: 'center',
smooth: true, smooth: true,
@ -117,8 +118,8 @@ export function HomeScreen() {
case 'ArrowDown': case 'ArrowDown':
case 'ArrowRight': { case 'ArrowRight': {
e.preventDefault(); e.preventDefault();
const nextIndex = Math.min(selectedIndex + 1, data.length - 1); const nextIndex = Math.min(index.peek() + 1, data.length - 1);
setSelectedIndex(nextIndex); index.value = nextIndex;
ref.current.scrollToIndex(nextIndex, { ref.current.scrollToIndex(nextIndex, {
align: 'center', align: 'center',
smooth: true, smooth: true,

View File

@ -0,0 +1,5 @@
import { ReactNode } from 'react';
export function WidgetContent({ children }: { children: ReactNode }) {
return <div className="h-full w-full">{children}</div>;
}

View File

@ -0,0 +1,112 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { useQueryClient } from '@tanstack/react-query';
import { ReactNode } from 'react';
import {
ArrowLeftIcon,
ArrowRightIcon,
HomeIcon,
HorizontalDotsIcon,
RefreshIcon,
TrashIcon,
} from '@shared/icons';
import { useWidget } from '@utils/hooks/useWidget';
export function WidgetHeader({
id,
title,
queryKey,
icon,
}: {
id: string;
title: string;
queryKey?: string;
icon?: ReactNode;
}) {
const queryClient = useQueryClient();
const { removeWidget } = useWidget();
const refresh = async () => {
if (queryKey) await queryClient.refetchQueries({ queryKey: [queryKey] });
};
const moveLeft = async () => {
removeWidget.mutate(id);
};
const moveRight = async () => {
removeWidget.mutate(id);
};
const deleteWidget = async () => {
removeWidget.mutate(id);
};
return (
<div className="flex h-11 w-full shrink-0 items-center justify-between border-b border-neutral-100 px-3 dark:border-neutral-900">
<div className="inline-flex items-center gap-4">
<div className="h-5 w-1 rounded-full bg-blue-500" />
<div className="inline-flex items-center gap-2">
{icon ? icon : <HomeIcon className="h-5 w-5" />}
<div className="text-sm font-medium">{title}</div>
</div>
</div>
<div>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button
type="button"
className="inline-flex h-6 w-6 items-center justify-center"
>
<HorizontalDotsIcon className="h-4 w-4" />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[220px] flex-col overflow-hidden rounded-xl border border-neutral-100 bg-white p-2 shadow-lg shadow-neutral-200/50 focus:outline-none dark:border-neutral-900 dark:bg-neutral-950 dark:shadow-neutral-900/50">
<DropdownMenu.Item asChild>
<button
type="button"
onClick={refresh}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
>
<RefreshIcon className="h-5 w-5" />
Refresh
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={moveLeft}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
>
<ArrowLeftIcon className="h-5 w-5" />
Move left
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={moveRight}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
>
<ArrowRightIcon className="h-5 w-5" />
Move right
</button>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-px bg-neutral-100 dark:bg-neutral-900" />
<DropdownMenu.Item asChild>
<button
type="button"
onClick={deleteWidget}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-red-500 hover:bg-red-500 hover:text-red-50 focus:outline-none"
>
<TrashIcon className="h-5 w-5" />
Delete
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import { WidgetContent } from './content';
import { WidgetHeader } from './header';
import { WidgetRoot } from './root';
export const Widget = {
Root: WidgetRoot,
Header: WidgetHeader,
Content: WidgetContent,
};

View File

@ -1,22 +1,23 @@
import { useSignal } from '@preact/signals-react';
import { Resizable } from 're-resizable'; import { Resizable } from 're-resizable';
import { ReactNode, useState } from 'react'; import { ReactNode } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export function WidgetWrapper({ export function WidgetRoot({
children, children,
className, className,
}: { }: {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
}) { }) {
const [width, setWidth] = useState(420); const width = useSignal(420);
return ( return (
<Resizable <Resizable
size={{ width: width, height: '100%' }} size={{ width: width.value, height: '100%' }}
onResizeStart={(e) => e.preventDefault()} onResizeStart={(e) => e.preventDefault()}
onResizeStop={(_e, _direction, _ref, d) => { onResizeStop={(_e, _direction, _ref, d) => {
setWidth((prevWidth) => prevWidth + d.width); width.value = width.peek() + d.width;
}} }}
minWidth={420} minWidth={420}
maxWidth={600} maxWidth={600}

View File

@ -1,2 +1,6 @@
export * from './ark'; export * from './ark';
export * from './provider'; export * from './provider';
export * from './components/widget';
export * from './components/widget/content';
export * from './components/widget/header';
export * from './components/widget/root';

View File

@ -38,7 +38,7 @@ const ArkProvider = ({ children }: PropsWithChildren<object>) => {
// start depot // start depot
if (_ark.settings.depot) { if (_ark.settings.depot) {
await ark.launchDepot(); await _ark.launchDepot();
await delay(2000); await delay(2000);
} }

View File

@ -0,0 +1,18 @@
export function AnnouncementIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M16.36 3.014A27.429 27.429 0 0 1 8.143 8.04l-4.67 1.825a5.126 5.126 0 0 0 1.7 6.34l1.631-.25m9.556-12.94c-.875.234-.824 3.262.114 6.764.938 3.501 2.408 6.15 3.283 5.915M16.36 3.014c.875-.234 2.345 2.414 3.284 5.915.938 3.502.989 6.53.113 6.765m0 0a27.428 27.428 0 0 0-8.595-.382m0 0L13.295 22H8.92l-2.116-6.044m4.358-.644c-.345.04-.69.085-1.034.138l-3.324.506" />
</svg>
);
}

View File

@ -1,15 +1,18 @@
import { SVGProps } from 'react'; export function ArrowLeftIcon(props: JSX.IntrinsicElements['svg']) {
export function ArrowLeftIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return ( return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
<path {...props}
d="M10 18.25L3.75 12M3.75 12L10 5.75M3.75 12H20.25" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> strokeWidth="2"
>
<path d="M8.83 6a30.23 30.23 0 0 0-5.62 5.406A.949.949 0 0 0 3 12m5.83 6a30.233 30.233 0 0 1-5.62-5.406A.949.949 0 0 1 3 12m0 0h18" />
</svg> </svg>
); );
} }

View File

@ -1,15 +1,18 @@
import { SVGProps } from 'react'; export function ArrowRightIcon(props: JSX.IntrinsicElements['svg']) {
export function ArrowRightIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return ( return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
<path {...props}
d="M14 5.75L20.25 12M20.25 12L14 18.25M20.25 12H3.75" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> strokeWidth="2"
>
<path d="M15.17 6a30.23 30.23 0 0 1 5.62 5.406c.14.174.21.384.21.594m-5.83 6a30.232 30.232 0 0 0 5.62-5.406A.949.949 0 0 0 21 12m0 0H3" />
</svg> </svg>
); );
} }

View File

@ -1,21 +1,18 @@
import { SVGProps } from 'react'; export function HomeIcon(props: JSX.IntrinsicElements['svg']) {
export function HomeIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg
{...props}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24" width="24"
height="24" height="24"
fill="none" fill="none"
viewBox="0 0 24 24" stroke="currentColor"
{...props} strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
> >
<path <path d="M9 17h6M7.606 5.65l-2.6 2.456c-.74.698-1.11 1.047-1.374 1.46a4 4 0 0 0-.513 1.191C3 11.233 3 11.742 3 12.76V14.6c0 2.24 0 3.36.436 4.216a4 4 0 0 0 1.748 1.748C6.04 21 7.16 21 9.4 21h5.2c2.24 0 3.36 0 4.216-.436a4 4 0 0 0 1.748-1.748C21 17.96 21 16.84 21 14.6v-1.841c0-1.017 0-1.526-.119-2.002a4 4 0 0 0-.513-1.19c-.265-.414-.634-.763-1.374-1.461l-2.6-2.456c-1.546-1.46-2.32-2.19-3.201-2.466a4 4 0 0 0-2.386 0c-.882.275-1.655 1.006-3.201 2.466Z" />
fill="currentColor"
fillRule="evenodd"
d="M10.108 1.999a3 3 0 013.784 0l6 4.875A3 3 0 0121 9.202V18a3 3 0 01-3 3H6a3 3 0 01-3-3V9.202a3 3 0 011.108-2.328l6-4.875zM8 15a1 1 0 100 2h8a1 1 0 100-2H8z"
clipRule="evenodd"
></path>
</svg> </svg>
); );
} }

View File

@ -84,3 +84,4 @@ export * from './info';
export * from './light'; export * from './light';
export * from './dark'; export * from './dark';
export * from './system'; export * from './system';
export * from './announcement';

View File

@ -1,14 +1,18 @@
import { SVGProps } from 'react'; export function RefreshIcon(props: JSX.IntrinsicElements['svg']) {
export function RefreshIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return ( return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
<path {...props}
fillRule="evenodd" xmlns="http://www.w3.org/2000/svg"
clipRule="evenodd" viewBox="0 0 24 24"
d="M11.979 4.5C7.83687 4.5 4.479 7.85786 4.479 12C4.479 16.1421 7.83687 19.5 11.979 19.5C15.2434 19.5 18.0225 17.4141 19.0524 14.5001C19.1905 14.1095 19.619 13.9048 20.0095 14.0429C20.4 14.1809 20.6047 14.6094 20.4667 14.9999C19.2315 18.4945 15.8988 21 11.979 21C7.00844 21 2.979 16.9706 2.979 12C2.979 7.02944 7.00844 3 11.979 3C13.709 3 15.1419 3.42256 16.4191 4.20651C17.1663 4.6651 17.8487 5.24046 18.5 5.90708V4C18.5 3.58579 18.8358 3.25 19.25 3.25C19.6642 3.25 20 3.58579 20 4V8C20 8.41421 19.6642 8.75 19.25 8.75H15.25C14.8358 8.75 14.5 8.41421 14.5 8C14.5 7.58579 14.8358 7.25 15.25 7.25H17.7068C17.0285 6.51595 16.3546 5.92693 15.6345 5.4849C14.6015 4.85088 13.4417 4.5 11.979 4.5Z" width="24"
fill="currentColor" height="24"
/> fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M17.5 2.474c.51 1.192.861 2.444 1.049 3.726a.479.479 0 0 1-.298.515l-.181.07M6.5 21.527A15 15 0 0 1 5.451 17.8a.48.48 0 0 1 .298-.515l.181-.07M14.5 7.67a15 15 0 0 0 3.57-.884m0 0a8 8 0 0 0-13.912 6.797m15.75-2.79A8 8 0 0 1 5.93 17.215m3.571-.885a15.002 15.002 0 0 0-3.57.884" />
</svg> </svg>
); );
} }

View File

@ -1,22 +1,18 @@
import { SVGProps } from 'react'; export function TimelineIcon(props: JSX.IntrinsicElements['svg']) {
export function TimeLineIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg
{...props}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24" width="24"
height="24" height="24"
fill="none" fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor" stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="1.5" strokeWidth="2"
d="M22.25 15C17.215 15 15 17.215 15 22.25 15 17.215 12.785 15 7.75 15 12.785 15 15 12.785 15 7.75c0 5.035 2.215 7.25 7.25 7.25zM11.25 6.5c-3.299 0-4.75 1.451-4.75 4.75 0-3.299-1.451-4.75-4.75-4.75 3.299 0 4.75-1.451 4.75-4.75 0 3.299 1.451 4.75 4.75 4.75z" >
clipRule="evenodd" <path d="M15 21v-5a3 3 0 1 0-6 0v5M7.606 5.65l-2.6 2.456c-.74.698-1.11 1.047-1.374 1.46a4 4 0 0 0-.513 1.191C3 11.233 3 11.742 3 12.76V14.6c0 2.24 0 3.36.436 4.216a4 4 0 0 0 1.748 1.748C6.04 21 7.16 21 9.4 21h5.2c2.24 0 3.36 0 4.216-.436a4 4 0 0 0 1.748-1.748C21 17.96 21 16.84 21 14.6v-1.841c0-1.017 0-1.526-.119-2.002a4 4 0 0 0-.513-1.19c-.265-.414-.634-.763-1.374-1.461l-2.6-2.456c-1.546-1.46-2.32-2.19-3.201-2.466a4 4 0 0 0-2.386 0c-.882.275-1.655 1.006-3.201 2.466Z" />
></path>
</svg> </svg>
); );
} }

View File

@ -1,19 +1,18 @@
import { SVGProps } from 'react'; export function TrashIcon(props: JSX.IntrinsicElements['svg']) {
export function TrashIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
> >
<path <path d="m16 6-1.106-2.211a3.236 3.236 0 0 0-5.788 0L8 6M4 6h16m-10 5v5m4-5v5M6 6h12v9c0 1.864 0 2.796-.305 3.53a4 4 0 0 1-2.164 2.165C14.796 21 13.864 21 12 21s-2.796 0-3.53-.305a4 4 0 0 1-2.166-2.164C6 17.796 6 16.864 6 15V6Z" />
d="M5.75 21.25L5.00156 21.2983C5.02702 21.6929 5.35453 22 5.75 22V21.25ZM18.25 21.25V22C18.6455 22 18.973 21.6929 18.9984 21.2983L18.25 21.25ZM2.75 5C2.33579 5 2 5.33579 2 5.75C2 6.16421 2.33579 6.5 2.75 6.5V5ZM21.25 6.5C21.6642 6.5 22 6.16421 22 5.75C22 5.33579 21.6642 5 21.25 5V6.5ZM10.5 10.75C10.5 10.3358 10.1642 10 9.75 10C9.33579 10 9 10.3358 9 10.75H10.5ZM9 16.25C9 16.6642 9.33579 17 9.75 17C10.1642 17 10.5 16.6642 10.5 16.25H9ZM15 10.75C15 10.3358 14.6642 10 14.25 10C13.8358 10 13.5 10.3358 13.5 10.75H15ZM13.5 16.25C13.5 16.6642 13.8358 17 14.25 17C14.6642 17 15 16.6642 15 16.25H13.5ZM15.1477 5.93694C15.2509 6.33808 15.6598 6.57957 16.0609 6.47633C16.4621 6.37308 16.7036 5.9642 16.6003 5.56306L15.1477 5.93694ZM4.00156 5.79829L5.00156 21.2983L6.49844 21.2017L5.49844 5.70171L4.00156 5.79829ZM5.75 22H18.25V20.5H5.75V22ZM18.9984 21.2983L19.9984 5.79829L18.5016 5.70171L17.5016 21.2017L18.9984 21.2983ZM19.25 5H4.75V6.5H19.25V5ZM2.75 6.5H4.75V5H2.75V6.5ZM19.25 6.5H21.25V5H19.25V6.5ZM9 10.75V16.25H10.5V10.75H9ZM13.5 10.75V16.25H15V10.75H13.5ZM12 3.5C13.5134 3.5 14.7868 4.53504 15.1477 5.93694L16.6003 5.56306C16.0731 3.51451 14.2144 2 12 2V3.5ZM8.85237 5.93694C9.21319 4.53504 10.4867 3.5 12 3.5V2C9.78568 2 7.92697 3.51451 7.39971 5.56306L8.85237 5.93694Z"
fill="currentColor"
/>
</svg> </svg>
); );
} }

View File

@ -1,10 +1,18 @@
import { Outlet } from 'react-router-dom'; import { type Platform } from '@tauri-apps/plugin-os';
import { Outlet, ScrollRestoration } from 'react-router-dom';
import { WindowTitleBar } from '@shared/titlebar';
export function AuthLayout() { export function AuthLayout({ platform }: { platform: Platform }) {
return ( return (
<div className="h-full w-full px-2.5 pb-2.5 pt-1"> <div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
<div className="flex h-full min-h-0 w-full rounded-lg bg-white p-3 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]"> {platform !== 'macos' ? (
<WindowTitleBar platform={platform} />
) : (
<div data-tauri-drag-region className="h-9 shrink-0" />
)}
<div className="h-full w-full">
<Outlet /> <Outlet />
<ScrollRestoration />
</div> </div>
</div> </div>
); );

View File

@ -1,3 +1,4 @@
import { Platform } from '@tauri-apps/plugin-os';
import { NavLink, Outlet, useNavigate } from 'react-router-dom'; import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { import {
@ -8,11 +9,18 @@ import {
SettingsIcon, SettingsIcon,
UserIcon, UserIcon,
} from '@shared/icons'; } from '@shared/icons';
import { WindowTitleBar } from '@shared/titlebar';
export function SettingsLayout() { export function SettingsLayout({ platform }: { platform: Platform }) {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
{platform !== 'macos' ? (
<WindowTitleBar platform={platform} />
) : (
<div data-tauri-drag-region className="h-9 shrink-0" />
)}
<div className="flex h-full min-h-0 w-full flex-col gap-8 overflow-y-auto"> <div className="flex h-full min-h-0 w-full flex-col gap-8 overflow-y-auto">
<div className="flex h-20 w-full items-center justify-between border-b border-neutral-200 px-2 pb-2 dark:border-neutral-900"> <div className="flex h-20 w-full items-center justify-between border-b border-neutral-200 px-2 pb-2 dark:border-neutral-900">
<div> <div>
@ -101,5 +109,6 @@ export function SettingsLayout() {
</div> </div>
<Outlet /> <Outlet />
</div> </div>
</div>
); );
} }

View File

@ -3,18 +3,18 @@ import { useInfiniteQuery } from '@tanstack/react-query';
import { FetchFilter } from 'nostr-fetch'; import { FetchFilter } from 'nostr-fetch';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useArk } from '@libs/ark'; import { Widget, useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedArticleNote } from '@shared/notes'; import { MemoizedArticleNote } from '@shared/notes';
import { TitleBar, WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
import { Widget } from '@utils/types'; import { WidgetProps } from '@utils/types';
export function ArticleWidget({ widget }: { widget: Widget }) { export function ArticleWidget({ props }: { props: WidgetProps }) {
const ark = useArk(); const ark = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['article', widget.id], queryKey: ['article', props.id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,
@ -24,7 +24,7 @@ export function ArticleWidget({ widget }: { widget: Widget }) {
pageParam: number; pageParam: number;
}) => { }) => {
let filter: FetchFilter; let filter: FetchFilter;
const content = JSON.parse(widget.content); const content = JSON.parse(props.content);
if (content.global) { if (content.global) {
filter = { filter = {
@ -62,8 +62,9 @@ export function ArticleWidget({ widget }: { widget: Widget }) {
); );
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id={widget.id} title={widget.title} /> <Widget.Header id={props.id} title={props.title} />
<Widget.Content>
<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">
@ -106,6 +107,7 @@ export function ArticleWidget({ widget }: { widget: Widget }) {
) : null} ) : null}
</div> </div>
</VList> </VList>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -2,18 +2,18 @@ import { useInfiniteQuery } from '@tanstack/react-query';
import { FetchFilter } from 'nostr-fetch'; import { FetchFilter } from 'nostr-fetch';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useArk } from '@libs/ark'; import { Widget, useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedFileNote } from '@shared/notes'; import { MemoizedFileNote } from '@shared/notes';
import { TitleBar, WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
import { Widget } from '@utils/types'; import { WidgetProps } from '@utils/types';
export function FileWidget({ widget }: { widget: Widget }) { export function FileWidget({ props }: { props: WidgetProps }) {
const ark = useArk(); const ark = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['media', widget.id], queryKey: ['media', props.id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,
@ -23,7 +23,7 @@ export function FileWidget({ widget }: { widget: Widget }) {
pageParam: number; pageParam: number;
}) => { }) => {
let filter: FetchFilter; let filter: FetchFilter;
const content = JSON.parse(widget.content); const content = JSON.parse(props.content);
if (content.global) { if (content.global) {
filter = { filter = {
@ -61,8 +61,9 @@ export function FileWidget({ widget }: { widget: Widget }) {
); );
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id={widget.id} title={widget.title} /> <Widget.Header id={props.id} title={props.title} />
<Widget.Content>
<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">
@ -105,6 +106,7 @@ export function FileWidget({ widget }: { widget: Widget }) {
) : null} ) : null}
</div> </div>
</VList> </VList>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -1,8 +1,8 @@
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 { useCallback, useMemo } from 'react'; import { useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useArk } from '@libs/ark'; import { Widget, useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { import {
MemoizedRepost, MemoizedRepost,
@ -10,15 +10,14 @@ import {
NoteSkeleton, NoteSkeleton,
UnknownNote, UnknownNote,
} from '@shared/notes'; } from '@shared/notes';
import { TitleBar, WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
import { Widget } from '@utils/types'; import { WidgetProps } from '@utils/types';
export function GroupWidget({ widget }: { widget: Widget }) { export function GroupWidget({ props }: { props: WidgetProps }) {
const ark = useArk(); const ark = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['groupfeeds', widget.id], queryKey: ['groupfeeds', props.id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,
@ -27,7 +26,7 @@ export function GroupWidget({ widget }: { widget: Widget }) {
signal: AbortSignal; signal: AbortSignal;
pageParam: number; pageParam: number;
}) => { }) => {
const authors = JSON.parse(widget.content); const authors = JSON.parse(props.content);
const events = await ark.getInfiniteEvents({ const events = await ark.getInfiniteEvents({
filter: { filter: {
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
@ -55,8 +54,7 @@ export function GroupWidget({ widget }: { widget: Widget }) {
[data] [data]
); );
const renderItem = useCallback( const renderItem = (event: NDKEvent) => {
(event: NDKEvent) => {
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return <MemoizedTextNote key={event.id} event={event} />; return <MemoizedTextNote key={event.id} event={event} />;
@ -65,13 +63,12 @@ export function GroupWidget({ widget }: { widget: Widget }) {
default: default:
return <UnknownNote key={event.id} event={event} />; return <UnknownNote key={event.id} event={event} />;
} }
}, };
[data]
);
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id={widget.id} title={widget.title} /> <Widget.Header id={props.id} title={props.title} />
<Widget.Content>
<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">
@ -102,6 +99,7 @@ export function GroupWidget({ widget }: { widget: Widget }) {
) : null} ) : null}
</div> </div>
</VList> </VList>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -1,19 +1,18 @@
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 { useCallback, useMemo } from 'react'; import { useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useArk } from '@libs/ark'; import { Widget, useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes'; import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes';
import { TitleBar, WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
import { Widget } from '@utils/types'; import { WidgetProps } from '@utils/types';
export function HashtagWidget({ widget }: { widget: Widget }) { export function HashtagWidget({ props }: { props: WidgetProps }) {
const ark = useArk(); const ark = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['hashtag', widget.id], queryKey: ['hashtag', props.id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,
@ -25,7 +24,7 @@ export function HashtagWidget({ widget }: { widget: Widget }) {
const events = await ark.getInfiniteEvents({ const events = await ark.getInfiniteEvents({
filter: { filter: {
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
'#t': [widget.content], '#t': [props.content],
}, },
limit: FETCH_LIMIT, limit: FETCH_LIMIT,
pageParam, pageParam,
@ -50,8 +49,7 @@ export function HashtagWidget({ widget }: { widget: Widget }) {
); );
// render event match event kind // render event match event kind
const renderItem = useCallback( const renderItem = (event: NDKEvent) => {
(event: NDKEvent) => {
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return <MemoizedTextNote key={event.id} event={event} />; return <MemoizedTextNote key={event.id} event={event} />;
@ -60,13 +58,12 @@ export function HashtagWidget({ widget }: { widget: Widget }) {
default: default:
return <UnknownNote key={event.id} event={event} />; return <UnknownNote key={event.id} event={event} />;
} }
}, };
[data]
);
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id={widget.id} title={widget.title} /> <Widget.Header id={props.id} title={props.title} />
<Widget.Content>
<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">
@ -78,7 +75,7 @@ export function HashtagWidget({ widget }: { widget: 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 {widget.content}. Oops, it looks like there are no events related to {props.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
@ -109,6 +106,7 @@ export function HashtagWidget({ widget }: { widget: Widget }) {
) : null} ) : null}
</div> </div>
</VList> </VList>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -7,10 +7,8 @@ export * from './file';
export * from './hashtag'; export * from './hashtag';
export * from './thread'; export * from './thread';
export * from './group'; export * from './group';
export * from './titleBar';
export * from './nostrBand/trendingAccounts'; export * from './nostrBand/trendingAccounts';
export * from './nostrBand/trendingNotes'; export * from './nostrBand/trendingNotes';
export * from './other/wrapper';
export * from './other/liveUpdater'; export * from './other/liveUpdater';
export * from './other/toggleWidgetList'; export * from './other/toggleWidgetList';
export * from './other/widgetList'; export * from './other/widgetList';

View File

@ -2,15 +2,14 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from 'virtua';
import { useArk } from '@libs/ark'; import { Widget, useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon, TimelineIcon } from '@shared/icons';
import { import {
MemoizedRepost, MemoizedRepost,
MemoizedTextNote, MemoizedTextNote,
NoteSkeleton, NoteSkeleton,
UnknownNote, UnknownNote,
} from '@shared/notes'; } from '@shared/notes';
import { LiveUpdater, TitleBar, WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
export function NewsfeedWidget() { export function NewsfeedWidget() {
@ -67,9 +66,13 @@ export function NewsfeedWidget() {
}; };
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id="9999" isLive /> <Widget.Header
<LiveUpdater status={status} /> id="9999"
title="Timeline"
icon={<TimelineIcon className="h-5 w-5" />}
/>
<Widget.Content>
<VList ref={ref} overscan={2} className="flex-1"> <VList ref={ref} overscan={2} className="flex-1">
{status === 'pending' ? ( {status === 'pending' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
@ -80,7 +83,7 @@ export function NewsfeedWidget() {
) : ( ) : (
allEvents.map((item) => renderItem(item)) allEvents.map((item) => renderItem(item))
)} )}
<div className="flex h-16 items-center justify-center px-3 pb-3"> <div className="flex h-16 items-center justify-center px-3 py-3">
{hasNextPage ? ( {hasNextPage ? (
<button <button
type="button" type="button"
@ -100,6 +103,7 @@ export function NewsfeedWidget() {
) : null} ) : null}
</div> </div>
</VList> </VList>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -1,19 +1,15 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { Widget } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { import { NostrBandUserProfile, type Profile } from '@shared/widgets';
NostrBandUserProfile, import { WidgetProps } from '@utils/types';
type Profile,
TitleBar,
WidgetWrapper,
} from '@shared/widgets';
import { Widget } from '@utils/types';
interface Response { interface Response {
profiles: Array<{ pubkey: string }>; profiles: Array<{ pubkey: string }>;
} }
export function TrendingAccountsWidget({ widget }: { widget: Widget }) { export function TrendingAccountsWidget({ props }: { props: WidgetProps }) {
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['trending-users'], queryKey: ['trending-users'],
queryFn: async () => { queryFn: async () => {
@ -32,8 +28,9 @@ export function TrendingAccountsWidget({ widget }: { widget: Widget }) {
}); });
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id={widget.id} title="Trending Accounts" /> <Widget.Header id={props.id} title="Trending Accounts" />
<Widget.Content>
<div className="flex-1"> <div 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 ">
@ -64,6 +61,7 @@ export function TrendingAccountsWidget({ widget }: { widget: Widget }) {
</VList> </VList>
)} )}
</div> </div>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -1,16 +1,16 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { Widget } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { MemoizedTextNote } from '@shared/notes'; import { MemoizedTextNote } from '@shared/notes';
import { TitleBar, WidgetWrapper } from '@shared/widgets'; import { WidgetProps } from '@utils/types';
import { Widget } from '@utils/types';
interface Response { interface Response {
notes: Array<{ event: NDKEvent }>; notes: Array<{ event: NDKEvent }>;
} }
export function TrendingNotesWidget({ widget }: { widget: Widget }) { export function TrendingNotesWidget({ props }: { props: WidgetProps }) {
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['trending-posts'], queryKey: ['trending-posts'],
queryFn: async () => { queryFn: async () => {
@ -29,8 +29,9 @@ export function TrendingNotesWidget({ widget }: { widget: Widget }) {
}); });
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id={widget.id} title="Trending Notes" /> <Widget.Header id={props.id} title="Trending Notes" />
<Widget.Content>
<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 ">
@ -53,9 +54,12 @@ export function TrendingNotesWidget({ widget }: { widget: Widget }) {
</div> </div>
</div> </div>
) : ( ) : (
data.map((item) => <MemoizedTextNote key={item.event.id} event={item.event} />) data.map((item) => (
<MemoizedTextNote key={item.event.id} event={item.event} />
))
)} )}
</VList> </VList>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -1,18 +1,17 @@
import { NDKEvent, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useArk } from '@libs/ark'; import { Widget, useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { AnnouncementIcon, ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { MemoizedNotifyNote, NoteSkeleton } from '@shared/notes'; import { MemoizedNotifyNote, NoteSkeleton } from '@shared/notes';
import { TitleBar, WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
import { sendNativeNotification } from '@utils/notification'; import { sendNativeNotification } from '@utils/notification';
export function NotificationWidget() { export function NotificationWidget() {
const ark = useArk();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const ark = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['notification'], queryKey: ['notification'],
@ -52,10 +51,10 @@ export function NotificationWidget() {
[data] [data]
); );
const renderEvent = useCallback((event: NDKEvent) => { const renderEvent = (event: NDKEvent) => {
if (event.pubkey === ark.account.pubkey) return null; if (event.pubkey === ark.account.pubkey) return null;
return <MemoizedNotifyNote key={event.id} event={event} />; return <MemoizedNotifyNote key={event.id} event={event} />;
}, []); };
useEffect(() => { useEffect(() => {
let sub: NDKSubscription = undefined; let sub: NDKSubscription = undefined;
@ -124,8 +123,13 @@ export function NotificationWidget() {
}, [status]); }, [status]);
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id="9998" title="Notification" isLive /> <Widget.Header
id="9998"
title="Notification"
icon={<AnnouncementIcon className="h-5 w-5" />}
/>
<Widget.Content>
<VList className="flex-1" overscan={2}> <VList className="flex-1" overscan={2}>
{status === 'pending' ? ( {status === 'pending' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
@ -163,6 +167,7 @@ export function NotificationWidget() {
) : null} ) : null}
</div> </div>
</VList> </VList>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -1,5 +1,5 @@
import { Widget } from '@libs/ark';
import { PlusIcon } from '@shared/icons'; import { PlusIcon } from '@shared/icons';
import { WidgetWrapper } from '@shared/widgets';
import { WIDGET_KIND } from '@utils/constants'; import { WIDGET_KIND } from '@utils/constants';
import { useWidget } from '@utils/hooks/useWidget'; import { useWidget } from '@utils/hooks/useWidget';
@ -7,7 +7,8 @@ export function ToggleWidgetList() {
const { addWidget } = useWidget(); const { addWidget } = useWidget();
return ( return (
<WidgetWrapper> <Widget.Root>
<Widget.Content>
<div className="relative flex h-full w-full flex-col items-center justify-center"> <div className="relative flex h-full w-full flex-col items-center justify-center">
<button <button
type="button" type="button"
@ -19,6 +20,7 @@ export function ToggleWidgetList() {
<PlusIcon className="h-5 w-5" /> <PlusIcon className="h-5 w-5" />
</button> </button>
</div> </div>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -1,15 +1,17 @@
import { Widget } from '@libs/ark';
import { ArticleIcon, MediaIcon, PlusIcon } from '@shared/icons'; import { ArticleIcon, MediaIcon, PlusIcon } from '@shared/icons';
import { AddGroupFeeds, AddHashtagFeeds, TitleBar, WidgetWrapper } from '@shared/widgets'; import { AddGroupFeeds, AddHashtagFeeds } from '@shared/widgets';
import { TOPICS, WIDGET_KIND } from '@utils/constants'; import { TOPICS, WIDGET_KIND } from '@utils/constants';
import { useWidget } from '@utils/hooks/useWidget'; import { useWidget } from '@utils/hooks/useWidget';
import { Widget } from '@utils/types'; import { WidgetProps } from '@utils/types';
export function WidgetList({ widget }: { widget: Widget }) { export function WidgetList({ props }: { props: WidgetProps }) {
const { replaceWidget } = useWidget(); const { replaceWidget } = useWidget();
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id={widget.id} title="Add widgets" /> <Widget.Header id={props.id} title="Add widgets" />
<Widget.Content>
<div className="flex-1 overflow-y-auto pb-10 scrollbar-none"> <div className="flex-1 overflow-y-auto pb-10 scrollbar-none">
<div className="flex flex-col gap-6 px-3"> <div className="flex flex-col gap-6 px-3">
<div className="rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900"> <div className="rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
@ -37,7 +39,7 @@ export function WidgetList({ widget }: { widget: Widget }) {
type="button" type="button"
onClick={() => onClick={() =>
replaceWidget.mutate({ replaceWidget.mutate({
currentId: widget.id, currentId: props.id,
widget: { widget: {
kind: WIDGET_KIND.topic, kind: WIDGET_KIND.topic,
title: topic.title, title: topic.title,
@ -60,8 +62,8 @@ export function WidgetList({ widget }: { widget: Widget }) {
Newsfeed Newsfeed
</h3> </h3>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<AddGroupFeeds currentWidgetId={widget.id} /> <AddGroupFeeds currentWidgetId={props.id} />
<AddHashtagFeeds currentWidgetId={widget.id} /> <AddHashtagFeeds currentWidgetId={props.id} />
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-neutral-50 px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:bg-neutral-950 dark:hover:shadow-neutral-800/50"> <div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-neutral-50 px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:bg-neutral-950 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5"> <div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100 dark:bg-neutral-900"> <div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100 dark:bg-neutral-900">
@ -73,7 +75,7 @@ export function WidgetList({ widget }: { widget: Widget }) {
type="button" type="button"
onClick={() => onClick={() =>
replaceWidget.mutate({ replaceWidget.mutate({
currentId: widget.id, currentId: props.id,
widget: { widget: {
kind: WIDGET_KIND.article, kind: WIDGET_KIND.article,
title: 'Articles', title: 'Articles',
@ -98,7 +100,7 @@ export function WidgetList({ widget }: { widget: Widget }) {
type="button" type="button"
onClick={() => onClick={() =>
replaceWidget.mutate({ replaceWidget.mutate({
currentId: widget.id, currentId: props.id,
widget: { widget: {
kind: WIDGET_KIND.file, kind: WIDGET_KIND.file,
title: 'Media', title: 'Media',
@ -116,6 +118,7 @@ export function WidgetList({ widget }: { widget: Widget }) {
</div> </div>
</div> </div>
</div> </div>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -1,7 +1,6 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useCallback } from 'react';
import { WVList } from 'virtua'; import { WVList } from 'virtua';
import { useArk } from '@libs/ark'; import { Widget, useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { import {
ChildNote, ChildNote,
@ -13,16 +12,14 @@ import {
} from '@shared/notes'; } from '@shared/notes';
import { ReplyList } from '@shared/notes/replies/list'; import { ReplyList } from '@shared/notes/replies/list';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { TitleBar, WidgetWrapper } from '@shared/widgets';
import { useEvent } from '@utils/hooks/useEvent'; import { useEvent } from '@utils/hooks/useEvent';
import { Widget } from '@utils/types'; import { type WidgetProps } from '@utils/types';
export function ThreadWidget({ widget }: { widget: Widget }) { export function ThreadWidget({ props }: { props: WidgetProps }) {
const { isFetching, isError, data } = useEvent(widget.content);
const ark = useArk(); const ark = useArk();
const { isFetching, isError, data } = useEvent(props.content);
const renderKind = useCallback( const renderKind = (event: NDKEvent) => {
(event: NDKEvent) => {
const thread = ark.getEventThread({ tags: event.tags }); const thread = ark.getEventThread({ tags: event.tags });
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
@ -48,13 +45,12 @@ export function ThreadWidget({ widget }: { widget: Widget }) {
default: default:
return null; return null;
} }
}, };
[data]
);
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id={widget.id} title={widget.title} /> <Widget.Header id={props.id} title={props.title} />
<Widget.Content>
<WVList className="flex-1 overflow-y-auto px-3 pb-5"> <WVList className="flex-1 overflow-y-auto px-3 pb-5">
{isFetching ? ( {isFetching ? (
<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">
@ -78,6 +74,7 @@ export function ThreadWidget({ widget }: { widget: Widget }) {
</> </>
)} )}
</WVList> </WVList>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -1,64 +0,0 @@
import { useArk } from '@libs/ark';
import { CancelIcon } from '@shared/icons';
import { User } from '@shared/user';
import { useWidget } from '@utils/hooks/useWidget';
export function TitleBar({
id,
title,
isLive,
}: {
id?: string;
title?: string;
isLive?: boolean;
}) {
const ark = useArk();
const { removeWidget } = useWidget();
return (
<div className="grid h-11 w-full shrink-0 grid-cols-3 items-center px-3">
<div className="col-span-1 flex justify-start">
{isLive ? (
<div className="flex items-center gap-1.5">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-teal-400 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-teal-500"></span>
</span>
<p className="text-xs font-medium text-teal-500">Live</p>
</div>
) : null}
</div>
<div className="col-span-1 flex justify-center">
{id === '9999' ? (
<div className="isolate flex -space-x-2">
{ark.account.contacts
?.slice(0, 8)
.map((item) => <User key={item} pubkey={item} variant="ministacked" />)}
{ark.account.contacts?.length > 8 ? (
<div className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-neutral-300 text-neutral-900 ring-1 ring-white dark:bg-neutral-700 dark:text-neutral-100 dark:ring-black">
<span className="text-[8px] font-medium">
+{ark.account.contacts?.length - 8}
</span>
</div>
) : null}
</div>
) : (
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{title}
</h3>
)}
</div>
<div className="col-span-1 flex justify-end">
{id !== '9999' && id !== '9998' ? (
<button
type="button"
onClick={() => removeWidget.mutate(id)}
className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded text-neutral-900 backdrop-blur-xl hover:bg-neutral-100 dark:text-neutral-100 dark:hover:bg-neutral-900"
>
<CancelIcon className="h-3 w-3" />
</button>
) : null}
</div>
</div>
);
}

View File

@ -1,8 +1,8 @@
import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react'; import { useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useArk } from '@libs/ark'; import { Widget, useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { import {
MemoizedRepost, MemoizedRepost,
@ -10,15 +10,14 @@ import {
NoteSkeleton, NoteSkeleton,
UnknownNote, UnknownNote,
} from '@shared/notes'; } from '@shared/notes';
import { TitleBar, WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
import { Widget } from '@utils/types'; import { type WidgetProps } from '@utils/types';
export function TopicWidget({ widget }: { widget: Widget }) { export function TopicWidget({ props }: { props: WidgetProps }) {
const ark = useArk(); const ark = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['topic', widget.id], queryKey: ['topic', props.id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,
@ -27,7 +26,7 @@ export function TopicWidget({ widget }: { widget: Widget }) {
signal: AbortSignal; signal: AbortSignal;
pageParam: number; pageParam: number;
}) => { }) => {
const hashtags: string[] = JSON.parse(widget.content as string); const hashtags: string[] = JSON.parse(props.content as string);
const filter: NDKFilter = { const filter: NDKFilter = {
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
'#t': hashtags.map((tag) => tag.replace('#', '')), '#t': hashtags.map((tag) => tag.replace('#', '')),
@ -55,8 +54,7 @@ export function TopicWidget({ widget }: { widget: Widget }) {
[data] [data]
); );
const renderItem = useCallback( const renderItem = (event: NDKEvent) => {
(event: NDKEvent) => {
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return <MemoizedTextNote key={event.id} event={event} />; return <MemoizedTextNote key={event.id} event={event} />;
@ -65,13 +63,12 @@ export function TopicWidget({ widget }: { widget: Widget }) {
default: default:
return <UnknownNote key={event.id} event={event} />; return <UnknownNote key={event.id} event={event} />;
} }
}, };
[data]
);
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id={widget.id} title={widget.title} /> <Widget.Header id={props.id} title={props.title} />
<Widget.Content>
<VList className="flex-1" overscan={2}> <VList className="flex-1" overscan={2}>
{status === 'pending' ? ( {status === 'pending' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
@ -102,6 +99,7 @@ export function TopicWidget({ widget }: { widget: Widget }) {
) : null} ) : null}
</div> </div>
</VList> </VList>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -2,7 +2,7 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { WVList } from 'virtua'; import { WVList } from 'virtua';
import { useArk } from '@libs/ark'; import { Widget, useArk } from '@libs/ark';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { import {
MemoizedRepost, MemoizedRepost,
@ -10,15 +10,15 @@ import {
NoteSkeleton, NoteSkeleton,
UnknownNote, UnknownNote,
} from '@shared/notes'; } from '@shared/notes';
import { TitleBar, UserProfile, WidgetWrapper } from '@shared/widgets'; import { UserProfile } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
import { Widget } from '@utils/types'; import { type WidgetProps } from '@utils/types';
export function UserWidget({ widget }: { widget: Widget }) { export function UserWidget({ props }: { props: WidgetProps }) {
const ark = useArk(); const ark = useArk();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['user-posts', widget.content], queryKey: ['user-posts', props.content],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ queryFn: async ({
signal, signal,
@ -30,7 +30,7 @@ export function UserWidget({ widget }: { widget: Widget }) {
const events = await ark.getInfiniteEvents({ const events = await ark.getInfiniteEvents({
filter: { filter: {
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
authors: [widget.content], authors: [props.content],
}, },
limit: FETCH_LIMIT, limit: FETCH_LIMIT,
pageParam, pageParam,
@ -68,11 +68,12 @@ export function UserWidget({ widget }: { widget: Widget }) {
); );
return ( return (
<WidgetWrapper> <Widget.Root>
<TitleBar id={widget.id} title={widget.title} /> <Widget.Header id={props.id} title={props.title} />
<Widget.Content>
<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={widget.content} /> <UserProfile pubkey={props.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">
@ -110,6 +111,7 @@ export function UserWidget({ widget }: { widget: Widget }) {
</div> </div>
</div> </div>
</WVList> </WVList>
</WidgetWrapper> </Widget.Content>
</Widget.Root>
); );
} }

View File

@ -30,7 +30,7 @@ export interface WidgetGroupItem {
icon?: string; icon?: string;
} }
export interface Widget { export interface WidgetProps {
id?: string; id?: string;
account_id?: number; account_id?: number;
kind: number; kind: number;