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

@ -18,6 +18,7 @@
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
},
"dependencies": {
"@dnd-kit/core": "^6.0.8",
"@getalby/sdk": "^2.4.0",
"@nostr-dev-kit/ndk": "^1.3.0",
"@nostr-fetch/adapter-ndk": "^0.12.2",
@ -29,7 +30,6 @@
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^4.35.3",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.4.0",
"@tiptap/extension-image": "^2.1.11",
"@tiptap/extension-mention": "^2.1.11",
@ -56,7 +56,6 @@
"react-player": "^2.13.0",
"react-router-dom": "^6.16.0",
"react-textarea-autosize": "^8.5.3",
"react-virtuoso": "^4.6.0",
"reactflow": "^11.8.3",
"remark-gfm": "^3.0.1",
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql#v1",
@ -64,6 +63,7 @@
"tauri-plugin-stronghold-api": "github:tauri-apps/tauri-plugin-stronghold#v1",
"tauri-plugin-upload-api": "github:tauri-apps/tauri-plugin-upload#v1",
"tippy.js": "^6.3.7",
"virtua": "^0.9.1",
"zustand": "^4.4.1"
},
"devDependencies": {

View File

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@dnd-kit/core':
specifier: ^6.0.8
version: 6.0.8(react-dom@18.2.0)(react@18.2.0)
'@getalby/sdk':
specifier: ^2.4.0
version: 2.4.0
@ -38,9 +41,6 @@ dependencies:
'@tanstack/react-query':
specifier: ^4.35.3
version: 4.35.3(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-virtual':
specifier: 3.0.0-beta.54
version: 3.0.0-beta.54(react@18.2.0)
'@tauri-apps/api':
specifier: ^1.4.0
version: 1.4.0
@ -119,9 +119,6 @@ dependencies:
react-textarea-autosize:
specifier: ^8.5.3
version: 8.5.3(@types/react@18.2.22)(react@18.2.0)
react-virtuoso:
specifier: ^4.6.0
version: 4.6.0(react-dom@18.2.0)(react@18.2.0)
reactflow:
specifier: ^11.8.3
version: 11.8.3(@types/react@18.2.22)(react-dom@18.2.0)(react@18.2.0)
@ -130,19 +127,22 @@ dependencies:
version: 3.0.1
tauri-plugin-sql-api:
specifier: github:tauri-apps/tauri-plugin-sql#v1
version: github.com/tauri-apps/tauri-plugin-sql/51e39b0b6ba542ffc6af1fa438933fdc1ae265a0
version: github.com/tauri-apps/tauri-plugin-sql/533198dd3b6cfca36d876918d22efcdaac43065a
tauri-plugin-store-api:
specifier: github:tauri-apps/tauri-plugin-store#v1
version: github.com/tauri-apps/tauri-plugin-store/a65ce9bfb168a9a3cd7ed4102b9f22770cc3abfa
version: github.com/tauri-apps/tauri-plugin-store/66e06b7830037fdae0b42b5499e23334eaf4e017
tauri-plugin-stronghold-api:
specifier: github:tauri-apps/tauri-plugin-stronghold#v1
version: github.com/tauri-apps/tauri-plugin-stronghold/96dd2cc891915e6fdfb78868b0bef6c5648335a2
version: github.com/tauri-apps/tauri-plugin-stronghold/4684fed1f5e7eb01885e40114accdcecb61962ed
tauri-plugin-upload-api:
specifier: github:tauri-apps/tauri-plugin-upload#v1
version: github.com/tauri-apps/tauri-plugin-upload/b53ebc6c2e716d95fd94b64d3b4b87cd57ae4feb
version: github.com/tauri-apps/tauri-plugin-upload/40c0bc302a9dd8304762951e450ee84d53c2037b
tippy.js:
specifier: ^6.3.7
version: 6.3.7
virtua:
specifier: ^0.9.1
version: 0.9.1(react-dom@18.2.0)(react@18.2.0)
zustand:
specifier: ^4.4.1
version: 4.4.1(@types/react@18.2.22)(react@18.2.0)
@ -378,6 +378,37 @@ packages:
to-fast-properties: 2.0.0
dev: true
/@dnd-kit/accessibility@3.0.1(react@18.2.0):
resolution: {integrity: sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==}
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.2.0
tslib: 2.6.2
dev: false
/@dnd-kit/core@6.0.8(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@dnd-kit/accessibility': 3.0.1(react@18.2.0)
'@dnd-kit/utilities': 3.2.1(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.6.2
dev: false
/@dnd-kit/utilities@3.2.1(react@18.2.0):
resolution: {integrity: sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==}
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.2.0
tslib: 2.6.2
dev: false
/@esbuild/android-arm64@0.18.20:
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'}
@ -1799,19 +1830,6 @@ packages:
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/@tanstack/react-virtual@3.0.0-beta.54(react@18.2.0):
resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@tanstack/virtual-core': 3.0.0-beta.54
react: 18.2.0
dev: false
/@tanstack/virtual-core@3.0.0-beta.54:
resolution: {integrity: sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==}
dev: false
/@tauri-apps/api@1.4.0:
resolution: {integrity: sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw==}
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
@ -5671,17 +5689,6 @@ packages:
- '@types/react'
dev: false
/react-virtuoso@4.6.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-paQbkLA8U6dRe9srltWgPeoFCtNKqUYIcOpUR01JyznzaXWVzgZQE0M9KGi9vMoq8vHvHkGzuxJ6jDCS6uzePg==}
engines: {node: '>=10'}
peerDependencies:
react: '>=16 || >=17 || >= 18'
react-dom: '>=16 || >=17 || >= 18'
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
@ -6467,6 +6474,16 @@ packages:
vfile-message: 3.1.4
dev: false
/virtua@0.9.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-uaFvh+5zCDEenQDgxfIs67kpci7d/3XjdnWP/TdDYLcoXdWKr5ddwiP1g+wybHpXmLqbfJ0X0njmlAvP7GwMdw==}
peerDependencies:
react: '>=16.14.0'
react-dom: '>=16.14.0'
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/vite-tsconfig-paths@4.2.1(typescript@5.2.2)(vite@4.4.9):
resolution: {integrity: sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==}
peerDependencies:
@ -6669,32 +6686,32 @@ packages:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false
github.com/tauri-apps/tauri-plugin-sql/51e39b0b6ba542ffc6af1fa438933fdc1ae265a0:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-sql/tar.gz/51e39b0b6ba542ffc6af1fa438933fdc1ae265a0}
github.com/tauri-apps/tauri-plugin-sql/533198dd3b6cfca36d876918d22efcdaac43065a:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-sql/tar.gz/533198dd3b6cfca36d876918d22efcdaac43065a}
name: tauri-plugin-sql-api
version: 0.0.0
dependencies:
'@tauri-apps/api': 1.4.0
dev: false
github.com/tauri-apps/tauri-plugin-store/a65ce9bfb168a9a3cd7ed4102b9f22770cc3abfa:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/a65ce9bfb168a9a3cd7ed4102b9f22770cc3abfa}
github.com/tauri-apps/tauri-plugin-store/66e06b7830037fdae0b42b5499e23334eaf4e017:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/66e06b7830037fdae0b42b5499e23334eaf4e017}
name: tauri-plugin-store-api
version: 0.0.0
dependencies:
'@tauri-apps/api': 1.4.0
dev: false
github.com/tauri-apps/tauri-plugin-stronghold/96dd2cc891915e6fdfb78868b0bef6c5648335a2:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-stronghold/tar.gz/96dd2cc891915e6fdfb78868b0bef6c5648335a2}
github.com/tauri-apps/tauri-plugin-stronghold/4684fed1f5e7eb01885e40114accdcecb61962ed:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-stronghold/tar.gz/4684fed1f5e7eb01885e40114accdcecb61962ed}
name: tauri-plugin-stronghold-api
version: 0.0.0
dependencies:
'@tauri-apps/api': 1.4.0
dev: false
github.com/tauri-apps/tauri-plugin-upload/b53ebc6c2e716d95fd94b64d3b4b87cd57ae4feb:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-upload/tar.gz/b53ebc6c2e716d95fd94b64d3b4b87cd57ae4feb}
github.com/tauri-apps/tauri-plugin-upload/40c0bc302a9dd8304762951e450ee84d53c2037b:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-upload/tar.gz/40c0bc302a9dd8304762951e450ee84d53c2037b}
name: tauri-plugin-upload-api
version: 0.0.0
dependencies:

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 key={event.id} event={event}>
<TextNote />
</NoteWrapper>
</div>
);
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 key={event.id} event={event}>
<FileNote />
</NoteWrapper>
</div>
);
case NDKKind.Article:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
</div>
);
default:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
</div>
);
}
},
@ -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 key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
</div>
);
},
[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">
) : 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">
<p className="text-center text-sm font-medium text-white">
There have been no new articles in the last 24 hours.
<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 key={event.id} event={event}>
<FileNote />
</NoteWrapper>
</div>
);
},
[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">
) : 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">
<p className="text-center text-sm font-medium text-white">
There have been no new files in the last 24 hours.
<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 key={event.id} event={event}>
<TextNote />
</NoteWrapper>
</div>
);
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 key={event.id} event={event}>
<FileNote />
</NoteWrapper>
</div>
);
case NDKKind.Article:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
</div>
);
default:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
</div>
);
}
},
@ -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">
) : 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">
<p className="text-center text-sm font-medium text-white">
There have been no new posts with this hashtag in the last 24 hours.
<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 key={event.id} event={event}>
<FileNote />
</NoteWrapper>
</div>
);
},
[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">
) : 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">
<p className="text-center text-sm text-white">
There have been no new posts.
<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>
)}
{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">
<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-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"
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 className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
) : null}
</div>
<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';
@ -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} />
<TextNote />
</NoteWrapper>
</div>
);
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 key={dbEvent.id} event={event}>
<FileNote />
</NoteWrapper>
</div>
);
case NDKKind.Article:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
<NoteWrapper key={dbEvent.id} event={event}>
<ArticleNote />
</NoteWrapper>
</div>
);
default:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
<NoteWrapper key={dbEvent.id} event={event}>
<UnknownNote />
</NoteWrapper>
</div>
);
}
},
@ -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">
) : 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">
<p className="text-center text-sm text-white">
There have been no new posts.
<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>
)}
{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">
<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-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"
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 className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
) : null}
</div>
<div className="h-16" />
</VList>
)}
</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 key={event.id} event={event}>
<FileNote />
</NoteWrapper>
</div>
);
},
[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">
) : 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">
<p className="text-center text-sm text-white">
There have been no new posts.
<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>
)}
{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">
<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-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"
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 className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
) : null}
</div>
<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';
@ -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} />
<TextNote />
</NoteWrapper>
</div>
);
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 key={dbEvent.id} event={event}>
<FileNote />
</NoteWrapper>
</div>
);
case NDKKind.Article:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
<NoteWrapper key={dbEvent.id} event={event}>
<ArticleNote />
</NoteWrapper>
</div>
);
default:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
<NoteWrapper key={dbEvent.id} event={event}>
<UnknownNote />
</NoteWrapper>
</div>
);
}
},
@ -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))}
</div>
</div>
)}
{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">
<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-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"
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 className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
) : null}
</div>
<div className="h-16" />
</VList>
)}
</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} />
<TextNote />
</NoteWrapper>
</div>
);
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 key={dbEvent.id} event={event}>
<FileNote />
</NoteWrapper>
</div>
);
case NDKKind.Article:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
<NoteWrapper key={dbEvent.id} event={event}>
<ArticleNote />
</NoteWrapper>
</div>
);
default:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
<NoteWrapper key={dbEvent.id} event={event}>
<UnknownNote />
</NoteWrapper>
</div>
);
}
},
@ -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))}
</div>
</div>
)}
{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">
<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-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"
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 className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
) : null}
</div>
<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';
@ -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 key={event.id} event={event}>
<TextNote />
</NoteWrapper>
</div>
);
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 key={event.id} event={event}>
<FileNote />
</NoteWrapper>
</div>
);
case NDKKind.Article:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
</div>
);
default:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
</div>
);
}
},
@ -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,7 +221,10 @@ export function useNostr() {
};
const fetchNIP04Messages = async (sender: string) => {
const senderMessages = await fetcher.fetchAllEvents(
let senderMessages: NostrEventExt<false>[] = [];
if (sender !== db.account.pubkey) {
senderMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
@ -229,6 +233,7 @@ export function useNostr() {
},
{ since: 0 }
);
}
const userMessages = await fetcher.fetchAllEvents(
relayUrls,