perf improve

This commit is contained in:
Ren Amamiya
2023-09-27 08:32:19 +07:00
parent 1d93f8cf6a
commit b339e842ca
28 changed files with 521 additions and 942 deletions

View File

@@ -26,7 +26,7 @@ export function UserLatestPosts({ pubkey }: { pubkey: string }) {
case NDKKind.Text:
return (
<NoteWrapper key={event.id} event={event}>
<TextNote content={event.content} />
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
@@ -34,19 +34,19 @@ export function UserLatestPosts({ pubkey }: { pubkey: string }) {
case 1063:
return (
<NoteWrapper key={event.id} event={event}>
<FileNote event={event} />
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<NoteWrapper key={event.id} event={event}>
<ArticleNote event={event} />
<ArticleNote />
</NoteWrapper>
);
default:
return (
<NoteWrapper key={event.id} event={event}>
<UnknownNote event={event} />
<UnknownNote />
</NoteWrapper>
);
}

View File

@@ -60,7 +60,7 @@ export const UserWithDrawer = memo(function UserWithDrawer({
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content className="fixed right-0 top-0 z-50 flex h-full w-[400px] items-center justify-center px-4 pb-4 pt-16">
<Dialog.Content className="fixed right-0 top-0 z-50 flex h-full w-[400px] animate-slideRightAndFade items-center justify-center px-4 pb-4 pt-16 transition-all">
<div className="h-full w-full overflow-y-auto rounded-lg border-t border-white/10 bg-white/20 py-3 backdrop-blur-3xl">
{status === 'loading' ? (
<div>

View File

@@ -13,7 +13,7 @@ export function BrowseScreen() {
to="/browse/"
className={({ isActive }) =>
twMerge(
'inline-flex h-8 w-20 items-center justify-center rounded-full text-sm font-semibold',
'inline-flex h-7 w-20 items-center justify-center rounded-full text-sm font-semibold',
isActive ? 'bg-white/10 hover:bg-white/20' : ' hover:bg-white/5'
)
}
@@ -24,7 +24,7 @@ export function BrowseScreen() {
to="/browse/relays"
className={({ isActive }) =>
twMerge(
'inline-flex h-8 w-20 items-center justify-center rounded-full text-sm font-semibold',
'inline-flex h-7 w-20 items-center justify-center rounded-full text-sm font-semibold',
isActive ? 'bg-white/10 hover:bg-white/20' : ' hover:bg-white/5'
)
}

View File

@@ -1,7 +1,12 @@
export function BrowseRelaysScreen() {
return (
<div className="h-full w-full">
<p>TODO</p>
<div className="grid h-full w-full grid-cols-3">
<div className="col-span-2 border-r border-white/5 pt-16">
<p>Content</p>
</div>
<div className="col-span-1 px-3 pt-6">
<h3 className="font-semibold text-white">Your relays</h3>
</div>
</div>
);
}

View File

@@ -7,6 +7,8 @@ import { UnknownsModal } from '@app/chats/components/unknowns';
import { useStorage } from '@libs/storage/provider';
import { LoaderIcon } from '@shared/icons';
import { useNostr } from '@utils/hooks/useNostr';
export function ChatsList() {
@@ -33,8 +35,10 @@ export function ChatsList() {
return (
<div className="flex flex-col">
<div className="inline-flex h-10 items-center gap-2.5 border-l-2 border-transparent pl-4">
<div className="relative h-7 w-7 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<div className="h-4 w-full animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<div className="relative inline-flex h-7 w-7 shrink-0 items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
</div>
<h5 className="text-white/50">Loading messages...</h5>
</div>
</div>
);

View File

@@ -2,6 +2,7 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage';
import { TextNote } from '@shared/notes';
import { User } from '@shared/user';
export function ChatMessageItem({
@@ -20,13 +21,12 @@ export function ChatMessageItem({
}
return (
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 backdrop-blur-xl hover:bg-white/10">
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-white/10">
<div className="flex flex-col">
<User pubkey={message.pubkey} time={message.created_at} variant="chat" />
<div className="-mt-[20px] pl-[49px]">
<p className="select-text whitespace-pre-line break-words text-base text-white">
{message.content}
</p>
<div className="-mt-5 flex items-start gap-3">
<div className="w-10 shrink-0" />
<TextNote content={message.content} />
</div>
</div>
</div>

View File

@@ -1,8 +1,8 @@
import { NDKSubscription } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { Virtuoso } from 'react-virtuoso';
import { VList, VListHandle } from 'virtua';
import { ChatMessageForm } from '@app/chats/components/messages/form';
import { ChatMessageItem } from '@app/chats/components/messages/item';
@@ -18,7 +18,7 @@ import { useStronghold } from '@stores/stronghold';
import { useNostr } from '@utils/hooks/useNostr';
export function ChatScreen() {
const virtuosoRef = useRef(null);
const listRef = useRef<VListHandle>(null);
const userPrivkey = useStronghold((state) => state.privkey);
const { db } = useStorage();
@@ -29,10 +29,8 @@ export function ChatScreen() {
return await fetchNIP04Messages(pubkey);
});
const itemContent = useCallback(
(index: string | number) => {
const message = data[index];
if (!message) return;
const renderItem = useCallback(
(message: NDKEvent) => {
return (
<ChatMessageItem
message={message}
@@ -44,12 +42,9 @@ export function ChatScreen() {
[data]
);
const computeItemKey = useCallback(
(index: string | number) => {
return data[index].id;
},
[data]
);
useEffect(() => {
if (data.length > 0) listRef.current?.scrollToIndex(data.length);
}, [data]);
useEffect(() => {
const sub: NDKSubscription = ndk.subscribe(
@@ -86,22 +81,17 @@ export function ChatScreen() {
<p className="text-sm font-medium text-white/50">Loading messages</p>
</div>
</div>
) : data.length === 0 ? (
<div className="absolute left-1/2 top-1/2 flex w-full -translate-x-1/2 -translate-y-1/2 transform flex-col gap-1 text-center">
<h3 className="mb-2 text-4xl">🙌</h3>
<p className="leading-none text-white/50">
You two didn&apos;t talk yet, let&apos;s send first message
</p>
</div>
) : (
<Virtuoso
ref={virtuosoRef}
data={data}
itemContent={itemContent}
computeItemKey={computeItemKey}
initialTopMostItemIndex={data.length - 1}
alignToBottom={true}
followOutput={true}
overscan={50}
increaseViewportBy={{ top: 200, bottom: 200 }}
className="scrollbar-hide relative overflow-y-auto"
components={{
EmptyPlaceholder: () => Empty,
}}
/>
<VList ref={listRef} className="scrollbar-hide h-full" mode="reverse">
{data.map((message) => renderItem(message))}
</VList>
)}
</div>
<div className="z-50 shrink-0 rounded-b-xl border-t border-white/5 bg-white/10 p-3 px-5 backdrop-blur-xl">
@@ -120,12 +110,3 @@ export function ChatScreen() {
</div>
);
}
const Empty = (
<div className="absolute left-1/2 top-1/2 flex w-full -translate-x-1/2 -translate-y-1/2 transform flex-col gap-1 text-center">
<h3 className="mb-2 text-4xl">🙌</h3>
<p className="leading-none text-white/50">
You two didn&apos;t talk yet, let&apos;s send first message
</p>
</div>
);

View File

@@ -1,8 +1,8 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { VList } from 'virtua';
import { UserProfile } from '@app/users/components/profile';
@@ -32,79 +32,35 @@ export function UserScreen() {
return [...events] as unknown as NDKEvent[];
});
const parentRef = useRef();
const virtualizer = useVirtualizer({
count: data ? data.length : 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={event.id} event={event} />
</div>
);
return <Repost key={event.id} event={event} />;
case 1063:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@@ -112,10 +68,7 @@ export function UserScreen() {
);
return (
<div
ref={parentRef}
className="scrollbar-hide relative h-full w-full overflow-y-auto bg-white/10 backdrop-blur-xl"
>
<div className="scrollbar-hide relative h-full w-full overflow-y-auto bg-white/10 backdrop-blur-xl">
<div data-tauri-drag-region className="absolute left-0 top-0 h-11 w-full" />
<UserProfile pubkey={pubkey} />
<div className="mt-6 h-full w-full border-t border-white/5 px-1.5">
@@ -129,7 +82,7 @@ export function UserScreen() {
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
) : data.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
@@ -140,22 +93,10 @@ export function UserScreen() {
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
<VList className="scrollbar-hide h-full">
{data.map((item) => renderItem(item))}
<div className="h-16" />
</VList>
)}
</div>
</div>

View File

@@ -21,6 +21,7 @@ export const NDKInstance = () => {
);
// TODO: fully support NIP-11
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function getExplicitRelays() {
try {
// get relays
@@ -62,7 +63,7 @@ export const NDKInstance = () => {
}
async function initNDK() {
const explicitRelayUrls = await getExplicitRelays();
const explicitRelayUrls = await db.getExplicitRelayUrls();
const instance = new NDK({
explicitRelayUrls,
cacheAdapter,

View File

@@ -23,7 +23,10 @@ export function Navigation() {
]);
return (
<Frame className="relative flex h-full w-[232px] flex-col" lighter>
<Frame
className="relative flex h-full w-[232px] flex-col border-r border-white/5"
lighter
>
<div
data-tauri-drag-region
className="inline-flex h-16 w-full items-center justify-end px-3"

View File

@@ -4,19 +4,19 @@ import { Link } from 'react-router-dom';
import { Image } from '@shared/image';
export function ArticleNote({ event }: { event: NDKEvent }) {
export function ArticleNote(props: { event?: NDKEvent }) {
const metadata = useMemo(() => {
const title = event.tags.find((tag) => tag[0] === 'title')?.[1];
const image = event.tags.find((tag) => tag[0] === 'image')?.[1];
const summary = event.tags.find((tag) => tag[0] === 'summary')?.[1];
const title = props.event.tags.find((tag) => tag[0] === 'title')?.[1];
const image = props.event.tags.find((tag) => tag[0] === 'image')?.[1];
const summary = props.event.tags.find((tag) => tag[0] === 'summary')?.[1];
let publishedAt: Date | string | number = event.tags.find(
let publishedAt: Date | string | number = props.event.tags.find(
(tag) => tag[0] === 'published_at'
)?.[1];
if (publishedAt) {
publishedAt = new Date(parseInt(publishedAt)).toLocaleDateString('en-US');
} else {
publishedAt = new Date(event.created_at * 1000).toLocaleDateString('en-US');
publishedAt = new Date(props.event.created_at * 1000).toLocaleDateString('en-US');
}
return {
@@ -25,11 +25,11 @@ export function ArticleNote({ event }: { event: NDKEvent }) {
publishedAt,
summary,
};
}, [event.id]);
}, [props.event.id]);
return (
<Link
to={`/notes/article/${event.id}`}
to={`/notes/article/${props.event.id}`}
preventScrollReset={true}
className="mb-2 mt-3 rounded-lg"
>

View File

@@ -6,8 +6,8 @@ import { Image } from '@shared/image';
import { fileType } from '@utils/nip94';
export function FileNote({ event }: { event: NDKEvent }) {
const url = event.tags.find((el) => el[0] === 'url')[1];
export function FileNote(props: { event?: NDKEvent }) {
const url = props.event.tags.find((el) => el[0] === 'url')[1];
const type = fileType(url);
if (type === 'image') {
@@ -15,7 +15,7 @@ export function FileNote({ event }: { event: NDKEvent }) {
<div className="mb-2 mt-3">
<Image
src={url}
alt={event.content}
alt={props.event.content}
className="h-auto w-full rounded-lg object-cover"
/>
</div>

View File

@@ -13,8 +13,8 @@ import {
import { parser } from '@utils/parser';
export function TextNote({ content }: { content: string }) {
const richContent = parser(content) ?? null;
export function TextNote(props: { content?: string }) {
const richContent = parser(props.content) ?? null;
if (!richContent) {
return (
@@ -26,7 +26,7 @@ export function TextNote({ content }: { content: string }) {
unwrapDisallowed={true}
linkTarget={'_blank'}
>
{content}
{props.content}
</ReactMarkdown>
</div>
);

View File

@@ -1,18 +1,18 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
export function UnknownNote({ event }: { event: NDKEvent }) {
export function UnknownNote(props: { event?: NDKEvent }) {
return (
<div className="flex w-full flex-col gap-2">
<div className="inline-flex flex-col gap-1 rounded-md bg-white/10 px-2 py-2 backdrop-blur-xl">
<span className="text-sm font-medium leading-none text-white/50">
Unknown kind: {event.kind}
Unknown kind: {props.event.kind}
</span>
<p className="text-sm leading-none text-white">
Lume isn&apos;t fully support this kind
</p>
</div>
<div className="select-text whitespace-pre-line break-all text-white">
<p>{event.content.toString()}</p>
<p>{props.event.content.toString()}</p>
</div>
</div>
);

View File

@@ -10,7 +10,7 @@ export function SubReply({ event }: { event: NDKEvent }) {
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="flex-1">
<TextNote content={event.content} />
<TextNote />
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { ReactNode } from 'react';
import { ReactElement, cloneElement } from 'react';
import { twMerge } from 'tailwind-merge';
import { ChildNote, NoteActions } from '@shared/notes';
@@ -13,7 +13,7 @@ export function NoteWrapper({
lighter = false,
}: {
event: NDKEvent;
children: ReactNode;
children: ReactElement;
repost?: boolean;
root?: string;
reply?: string;
@@ -34,7 +34,10 @@ export function NoteWrapper({
<div className="-mt-5 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="relative z-20 flex-1">
{children}
{cloneElement(
children,
event.kind === 1 ? { content: event.content } : { event: event }
)}
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
@@ -25,27 +25,13 @@ export function GlobalArticlesWidget({ params }: { params: Widget }) {
{ refetchOnWindowFocus: false }
);
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data ? data.length : 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(event: NDKEvent) => {
return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
},
[data]
@@ -54,40 +40,32 @@ export function GlobalArticlesWidget({ params }: { params: Widget }) {
return (
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm font-medium text-white">
There have been no new articles in the last 24 hours.
) : data.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="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
<VList className="scrollbar-hide h-full">
{data.map((item) => renderItem(item))}
<div className="h-16" />
</VList>
)}
</div>
</WidgetWrapper>

View File

@@ -1,7 +1,7 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
@@ -26,27 +26,13 @@ export function GlobalFilesWidget({ params }: { params: Widget }) {
{ refetchOnWindowFocus: false }
);
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data ? data.length : 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(event: NDKEvent) => {
return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
},
[data]
@@ -55,40 +41,32 @@ export function GlobalFilesWidget({ params }: { params: Widget }) {
return (
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm font-medium text-white">
There have been no new files in the last 24 hours.
) : data.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="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
<VList className="scrollbar-hide h-full">
{data.map((item) => renderItem(item))}
<div className="h-16" />
</VList>
)}
</div>
</WidgetWrapper>

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
@@ -35,79 +35,35 @@ export function GlobalHashtagWidget({ params }: { params: Widget }) {
{ refetchOnWindowFocus: false }
);
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data ? data.length : 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={event.id} event={event} />
</div>
);
return <Repost key={event.id} event={event} />;
case 1063:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@@ -117,40 +73,33 @@ export function GlobalHashtagWidget({ params }: { params: Widget }) {
return (
<WidgetWrapper>
<TitleBar id={params.id} title={params.title + ' in 24 hours ago'} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm font-medium text-white">
There have been no new posts with this hashtag in the last 24 hours.
) : data.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="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
<VList className="scrollbar-hide h-full">
{data.map((item) => renderItem(item))}
<div className="h-16" />
</VList>
)}
</div>
</WidgetWrapper>

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo } from 'react';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
@@ -27,27 +27,15 @@ export function LocalArticlesWidget({ params }: { params: Widget }) {
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(dbEvent: DBEvent) => {
const event: NDKEvent = JSON.parse(dbEvent.event as string);
return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
},
[data]
@@ -56,75 +44,59 @@ export function LocalArticlesWidget({ params }: { params: Widget }) {
return (
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white">
There have been no new posts.
) : dbEvents.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="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
<VList className="scrollbar-hide h-full">
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
</>
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
</>
)}
</button>
) : null}
</div>
</div>
<div className="h-16" />
</VList>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</div>
</WidgetWrapper>
);

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo } from 'react';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
@@ -36,80 +36,42 @@ export function LocalFeedsWidget({ params }: { params: Widget }) {
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const dbEvent: DBEvent = dbEvents[index];
if (!dbEvent) return;
(dbEvent: DBEvent) => {
const event: NDKEvent = JSON.parse(dbEvent.event as string);
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
<NoteWrapper
key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id}
event={event}
root={dbEvent.root_id}
reply={dbEvent.reply_id}
>
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={dbEvent.id} event={event} />
</div>
);
return <Repost key={dbEvent.id} event={event} />;
case 1063:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@@ -119,75 +81,59 @@ export function LocalFeedsWidget({ params }: { params: Widget }) {
return (
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white">
There have been no new posts.
) : dbEvents.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="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
<VList className="scrollbar-hide h-full">
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
</>
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
</>
)}
</button>
) : null}
</div>
</div>
<div className="h-16" />
</VList>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</div>
</WidgetWrapper>
);

View File

@@ -1,7 +1,7 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo } from 'react';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
@@ -27,27 +27,15 @@ export function LocalFilesWidget({ params }: { params: Widget }) {
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(dbEvent: DBEvent) => {
const event: NDKEvent = JSON.parse(dbEvent.event as string);
return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
},
[data]
@@ -56,75 +44,59 @@ export function LocalFilesWidget({ params }: { params: Widget }) {
return (
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white">
There have been no new posts.
) : dbEvents.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="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
<VList className="scrollbar-hide h-full">
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
</>
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
</>
)}
</button>
) : null}
</div>
</div>
<div className="h-16" />
</VList>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</div>
</WidgetWrapper>
);

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo } from 'react';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
@@ -35,80 +35,42 @@ export function LocalFollowsWidget({ params }: { params: Widget }) {
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const dbEvent: DBEvent = dbEvents[index];
if (!dbEvent) return;
(dbEvent: DBEvent) => {
const event: NDKEvent = JSON.parse(dbEvent.event as string);
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
<NoteWrapper
key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id}
event={event}
root={dbEvent.root_id}
reply={dbEvent.reply_id}
>
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={dbEvent.id} event={event} />
</div>
);
return <Repost key={dbEvent.id} event={event} />;
case 1063:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@@ -118,7 +80,7 @@ export function LocalFollowsWidget({ params }: { params: Widget }) {
return (
<WidgetWrapper>
<TitleBar id={params.id} title="Follows" />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
@@ -140,59 +102,37 @@ export function LocalFollowsWidget({ params }: { params: Widget }) {
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
<VList className="scrollbar-hide h-full">
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
</>
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
</>
)}
</button>
) : null}
</div>
</div>
<div className="h-16" />
</VList>
)}
{isFetchingNextPage && (
<div className="mb-20 px-3">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
) : null}
</div>
</div>
</WidgetWrapper>
);

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
@@ -38,80 +38,42 @@ export function LocalNetworkWidget() {
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const dbEvent: DBEvent = dbEvents[index];
if (!dbEvent) return;
(dbEvent: DBEvent) => {
const event: NDKEvent = JSON.parse(dbEvent.event as string);
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id + index}
data-index={index}
ref={virtualizer.measureElement}
<NoteWrapper
key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id}
event={event}
root={dbEvent.root_id}
reply={dbEvent.reply_id}
>
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={dbEvent.id} event={event} />
</div>
);
return <Repost key={dbEvent.id} event={event} />;
case 1063:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@@ -125,7 +87,7 @@ export function LocalNetworkWidget() {
const filter: NDKFilter = {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: db.account.network,
since: db.account.last_login_at ?? Math.floor(Date.now() / 1000),
since: Math.floor(Date.now() / 1000),
};
sub(filter, async (event) => {
@@ -138,7 +100,7 @@ export function LocalNetworkWidget() {
return (
<WidgetWrapper>
<TitleBar title="👋 Network" />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
@@ -160,59 +122,37 @@ export function LocalNetworkWidget() {
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
<VList className="scrollbar-hide h-full">
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
</>
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
</>
)}
</button>
) : null}
</div>
</div>
<div className="h-16" />
</VList>
)}
{isFetchingNextPage && (
<div className="mb-20 px-3">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
) : null}
</div>
</div>
</WidgetWrapper>
);

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
@@ -41,79 +41,35 @@ export function LocalUserWidget({ params }: { params: Widget }) {
}
);
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data ? data.length : 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={event.id} event={event} />
</div>
);
return <Repost key={event.id} event={event} />;
case 1063:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@@ -123,7 +79,7 @@ export function LocalUserWidget({ params }: { params: Widget }) {
return (
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="px-3 pt-1.5">
<UserProfile pubkey={params.content} />
</div>
@@ -136,7 +92,7 @@ export function LocalUserWidget({ params }: { params: Widget }) {
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
) : data.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
@@ -147,22 +103,10 @@ export function LocalUserWidget({ params }: { params: Widget }) {
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
<VList className="scrollbar-hide h-full">
{data.map((item) => renderItem(item))}
<div className="h-16" />
</VList>
)}
</div>
</div>

View File

@@ -9,6 +9,7 @@ import {
import { message, open } from '@tauri-apps/api/dialog';
import { Body, fetch } from '@tauri-apps/api/http';
import { LRUCache } from 'lru-cache';
import { NostrEventExt } from 'nostr-fetch';
import { nip19 } from 'nostr-tools';
import { useMemo } from 'react';
@@ -220,15 +221,19 @@ export function useNostr() {
};
const fetchNIP04Messages = async (sender: string) => {
const senderMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
authors: [sender],
'#p': [db.account.pubkey],
},
{ since: 0 }
);
let senderMessages: NostrEventExt<false>[] = [];
if (sender !== db.account.pubkey) {
senderMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
authors: [sender],
'#p': [db.account.pubkey],
},
{ since: 0 }
);
}
const userMessages = await fetcher.fetchAllEvents(
relayUrls,