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

101
pnpm-lock.yaml generated
View File

@@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
dependencies: dependencies:
'@dnd-kit/core':
specifier: ^6.0.8
version: 6.0.8(react-dom@18.2.0)(react@18.2.0)
'@getalby/sdk': '@getalby/sdk':
specifier: ^2.4.0 specifier: ^2.4.0
version: 2.4.0 version: 2.4.0
@@ -38,9 +41,6 @@ dependencies:
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^4.35.3 specifier: ^4.35.3
version: 4.35.3(react-dom@18.2.0)(react@18.2.0) 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': '@tauri-apps/api':
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0 version: 1.4.0
@@ -119,9 +119,6 @@ dependencies:
react-textarea-autosize: react-textarea-autosize:
specifier: ^8.5.3 specifier: ^8.5.3
version: 8.5.3(@types/react@18.2.22)(react@18.2.0) 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: reactflow:
specifier: ^11.8.3 specifier: ^11.8.3
version: 11.8.3(@types/react@18.2.22)(react-dom@18.2.0)(react@18.2.0) 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 version: 3.0.1
tauri-plugin-sql-api: tauri-plugin-sql-api:
specifier: github:tauri-apps/tauri-plugin-sql#v1 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: tauri-plugin-store-api:
specifier: github:tauri-apps/tauri-plugin-store#v1 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: tauri-plugin-stronghold-api:
specifier: github:tauri-apps/tauri-plugin-stronghold#v1 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: tauri-plugin-upload-api:
specifier: github:tauri-apps/tauri-plugin-upload#v1 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: tippy.js:
specifier: ^6.3.7 specifier: ^6.3.7
version: 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: zustand:
specifier: ^4.4.1 specifier: ^4.4.1
version: 4.4.1(@types/react@18.2.22)(react@18.2.0) version: 4.4.1(@types/react@18.2.22)(react@18.2.0)
@@ -378,6 +378,37 @@ packages:
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
dev: true 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: /@esbuild/android-arm64@0.18.20:
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -1799,19 +1830,6 @@ packages:
use-sync-external-store: 1.2.0(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0)
dev: false 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: /@tauri-apps/api@1.4.0:
resolution: {integrity: sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw==} resolution: {integrity: sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw==}
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
@@ -5671,17 +5689,6 @@ packages:
- '@types/react' - '@types/react'
dev: false 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: /react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -6467,6 +6474,16 @@ packages:
vfile-message: 3.1.4 vfile-message: 3.1.4
dev: false 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): /vite-tsconfig-paths@4.2.1(typescript@5.2.2)(vite@4.4.9):
resolution: {integrity: sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==} resolution: {integrity: sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==}
peerDependencies: peerDependencies:
@@ -6669,32 +6686,32 @@ packages:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false dev: false
github.com/tauri-apps/tauri-plugin-sql/51e39b0b6ba542ffc6af1fa438933fdc1ae265a0: github.com/tauri-apps/tauri-plugin-sql/533198dd3b6cfca36d876918d22efcdaac43065a:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-sql/tar.gz/51e39b0b6ba542ffc6af1fa438933fdc1ae265a0} resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-sql/tar.gz/533198dd3b6cfca36d876918d22efcdaac43065a}
name: tauri-plugin-sql-api name: tauri-plugin-sql-api
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@tauri-apps/api': 1.4.0 '@tauri-apps/api': 1.4.0
dev: false dev: false
github.com/tauri-apps/tauri-plugin-store/a65ce9bfb168a9a3cd7ed4102b9f22770cc3abfa: github.com/tauri-apps/tauri-plugin-store/66e06b7830037fdae0b42b5499e23334eaf4e017:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/a65ce9bfb168a9a3cd7ed4102b9f22770cc3abfa} resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/66e06b7830037fdae0b42b5499e23334eaf4e017}
name: tauri-plugin-store-api name: tauri-plugin-store-api
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@tauri-apps/api': 1.4.0 '@tauri-apps/api': 1.4.0
dev: false dev: false
github.com/tauri-apps/tauri-plugin-stronghold/96dd2cc891915e6fdfb78868b0bef6c5648335a2: github.com/tauri-apps/tauri-plugin-stronghold/4684fed1f5e7eb01885e40114accdcecb61962ed:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-stronghold/tar.gz/96dd2cc891915e6fdfb78868b0bef6c5648335a2} resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-stronghold/tar.gz/4684fed1f5e7eb01885e40114accdcecb61962ed}
name: tauri-plugin-stronghold-api name: tauri-plugin-stronghold-api
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@tauri-apps/api': 1.4.0 '@tauri-apps/api': 1.4.0
dev: false dev: false
github.com/tauri-apps/tauri-plugin-upload/b53ebc6c2e716d95fd94b64d3b4b87cd57ae4feb: github.com/tauri-apps/tauri-plugin-upload/40c0bc302a9dd8304762951e450ee84d53c2037b:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-upload/tar.gz/b53ebc6c2e716d95fd94b64d3b4b87cd57ae4feb} resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-upload/tar.gz/40c0bc302a9dd8304762951e450ee84d53c2037b}
name: tauri-plugin-upload-api name: tauri-plugin-upload-api
version: 0.0.0 version: 0.0.0
dependencies: dependencies:

View File

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

View File

@@ -60,7 +60,7 @@ export const UserWithDrawer = memo(function UserWithDrawer({
</button> </button>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <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"> <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' ? ( {status === 'loading' ? (
<div> <div>

View File

@@ -13,7 +13,7 @@ export function BrowseScreen() {
to="/browse/" to="/browse/"
className={({ isActive }) => className={({ isActive }) =>
twMerge( 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' isActive ? 'bg-white/10 hover:bg-white/20' : ' hover:bg-white/5'
) )
} }
@@ -24,7 +24,7 @@ export function BrowseScreen() {
to="/browse/relays" to="/browse/relays"
className={({ isActive }) => className={({ isActive }) =>
twMerge( 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' isActive ? 'bg-white/10 hover:bg-white/20' : ' hover:bg-white/5'
) )
} }

View File

@@ -1,7 +1,12 @@
export function BrowseRelaysScreen() { export function BrowseRelaysScreen() {
return ( return (
<div className="h-full w-full"> <div className="grid h-full w-full grid-cols-3">
<p>TODO</p> <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> </div>
); );
} }

View File

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

View File

@@ -2,6 +2,7 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage'; import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage';
import { TextNote } from '@shared/notes';
import { User } from '@shared/user'; import { User } from '@shared/user';
export function ChatMessageItem({ export function ChatMessageItem({
@@ -20,13 +21,12 @@ export function ChatMessageItem({
} }
return ( 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"> <div className="flex flex-col">
<User pubkey={message.pubkey} time={message.created_at} variant="chat" /> <User pubkey={message.pubkey} time={message.created_at} variant="chat" />
<div className="-mt-[20px] pl-[49px]"> <div className="-mt-5 flex items-start gap-3">
<p className="select-text whitespace-pre-line break-words text-base text-white"> <div className="w-10 shrink-0" />
{message.content} <TextNote content={message.content} />
</p>
</div> </div>
</div> </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 { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom'; 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 { ChatMessageForm } from '@app/chats/components/messages/form';
import { ChatMessageItem } from '@app/chats/components/messages/item'; import { ChatMessageItem } from '@app/chats/components/messages/item';
@@ -18,7 +18,7 @@ import { useStronghold } from '@stores/stronghold';
import { useNostr } from '@utils/hooks/useNostr'; import { useNostr } from '@utils/hooks/useNostr';
export function ChatScreen() { export function ChatScreen() {
const virtuosoRef = useRef(null); const listRef = useRef<VListHandle>(null);
const userPrivkey = useStronghold((state) => state.privkey); const userPrivkey = useStronghold((state) => state.privkey);
const { db } = useStorage(); const { db } = useStorage();
@@ -29,10 +29,8 @@ export function ChatScreen() {
return await fetchNIP04Messages(pubkey); return await fetchNIP04Messages(pubkey);
}); });
const itemContent = useCallback( const renderItem = useCallback(
(index: string | number) => { (message: NDKEvent) => {
const message = data[index];
if (!message) return;
return ( return (
<ChatMessageItem <ChatMessageItem
message={message} message={message}
@@ -44,12 +42,9 @@ export function ChatScreen() {
[data] [data]
); );
const computeItemKey = useCallback( useEffect(() => {
(index: string | number) => { if (data.length > 0) listRef.current?.scrollToIndex(data.length);
return data[index].id; }, [data]);
},
[data]
);
useEffect(() => { useEffect(() => {
const sub: NDKSubscription = ndk.subscribe( const sub: NDKSubscription = ndk.subscribe(
@@ -86,22 +81,17 @@ export function ChatScreen() {
<p className="text-sm font-medium text-white/50">Loading messages</p> <p className="text-sm font-medium text-white/50">Loading messages</p>
</div> </div>
</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 <VList ref={listRef} className="scrollbar-hide h-full" mode="reverse">
ref={virtuosoRef} {data.map((message) => renderItem(message))}
data={data} </VList>
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,
}}
/>
)} )}
</div> </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"> <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> </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 { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useCallback } from 'react';
import { useCallback, useRef } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { VList } from 'virtua';
import { UserProfile } from '@app/users/components/profile'; import { UserProfile } from '@app/users/components/profile';
@@ -32,79 +32,35 @@ export function UserScreen() {
return [...events] as unknown as NDKEvent[]; 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 // render event match event kind
const renderItem = useCallback( const renderItem = useCallback(
(index: string | number) => { (event: NDKEvent) => {
const event: NDKEvent = data[index];
if (!event) return;
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return ( return (
<div <NoteWrapper key={event.id} event={event}>
key={event.id + index} <TextNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<TextNote content={event.content} />
</NoteWrapper> </NoteWrapper>
</div>
); );
case NDKKind.Repost: case NDKKind.Repost:
return ( return <Repost key={event.id} event={event} />;
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={event.id} event={event} />
</div>
);
case 1063: case 1063:
return ( return (
<div <NoteWrapper key={event.id} event={event}>
key={event.id + index} <FileNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
case NDKKind.Article: case NDKKind.Article:
return ( return (
<div <NoteWrapper key={event.id} event={event}>
key={event.id + index} <ArticleNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
default: default:
return ( return (
<div <NoteWrapper key={event.id} event={event}>
key={event.id + index} <UnknownNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
} }
}, },
@@ -112,10 +68,7 @@ export function UserScreen() {
); );
return ( return (
<div <div className="scrollbar-hide relative h-full w-full overflow-y-auto bg-white/10 backdrop-blur-xl">
ref={parentRef}
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" /> <div data-tauri-drag-region className="absolute left-0 top-0 h-11 w-full" />
<UserProfile pubkey={pubkey} /> <UserProfile pubkey={pubkey} />
<div className="mt-6 h-full w-full border-t border-white/5 px-1.5"> <div className="mt-6 h-full w-full border-t border-white/5 px-1.5">
@@ -129,7 +82,7 @@ export function UserScreen() {
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
) : items.length === 0 ? ( ) : data.length === 0 ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl"> <div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
@@ -140,22 +93,10 @@ export function UserScreen() {
</div> </div>
</div> </div>
) : ( ) : (
<div <VList className="scrollbar-hide h-full">
style={{ {data.map((item) => renderItem(item))}
position: 'relative', <div className="h-16" />
width: '100%', </VList>
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>
)} )}
</div> </div>
</div> </div>

View File

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

View File

@@ -23,7 +23,10 @@ export function Navigation() {
]); ]);
return ( 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 <div
data-tauri-drag-region data-tauri-drag-region
className="inline-flex h-16 w-full items-center justify-end px-3" 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'; import { Image } from '@shared/image';
export function ArticleNote({ event }: { event: NDKEvent }) { export function ArticleNote(props: { event?: NDKEvent }) {
const metadata = useMemo(() => { const metadata = useMemo(() => {
const title = event.tags.find((tag) => tag[0] === 'title')?.[1]; const title = props.event.tags.find((tag) => tag[0] === 'title')?.[1];
const image = event.tags.find((tag) => tag[0] === 'image')?.[1]; const image = props.event.tags.find((tag) => tag[0] === 'image')?.[1];
const summary = event.tags.find((tag) => tag[0] === 'summary')?.[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' (tag) => tag[0] === 'published_at'
)?.[1]; )?.[1];
if (publishedAt) { if (publishedAt) {
publishedAt = new Date(parseInt(publishedAt)).toLocaleDateString('en-US'); publishedAt = new Date(parseInt(publishedAt)).toLocaleDateString('en-US');
} else { } else {
publishedAt = new Date(event.created_at * 1000).toLocaleDateString('en-US'); publishedAt = new Date(props.event.created_at * 1000).toLocaleDateString('en-US');
} }
return { return {
@@ -25,11 +25,11 @@ export function ArticleNote({ event }: { event: NDKEvent }) {
publishedAt, publishedAt,
summary, summary,
}; };
}, [event.id]); }, [props.event.id]);
return ( return (
<Link <Link
to={`/notes/article/${event.id}`} to={`/notes/article/${props.event.id}`}
preventScrollReset={true} preventScrollReset={true}
className="mb-2 mt-3 rounded-lg" className="mb-2 mt-3 rounded-lg"
> >

View File

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

View File

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

View File

@@ -1,18 +1,18 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
export function UnknownNote({ event }: { event: NDKEvent }) { export function UnknownNote(props: { event?: NDKEvent }) {
return ( return (
<div className="flex w-full flex-col gap-2"> <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"> <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"> <span className="text-sm font-medium leading-none text-white/50">
Unknown kind: {event.kind} Unknown kind: {props.event.kind}
</span> </span>
<p className="text-sm leading-none text-white"> <p className="text-sm leading-none text-white">
Lume isn&apos;t fully support this kind Lume isn&apos;t fully support this kind
</p> </p>
</div> </div>
<div className="select-text whitespace-pre-line break-all text-white"> <div className="select-text whitespace-pre-line break-all text-white">
<p>{event.content.toString()}</p> <p>{props.event.content.toString()}</p>
</div> </div>
</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="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" /> <div className="w-11 shrink-0" />
<div className="flex-1"> <div className="flex-1">
<TextNote content={event.content} /> <TextNote />
<NoteActions id={event.id} pubkey={event.pubkey} /> <NoteActions id={event.id} pubkey={event.pubkey} />
</div> </div>
</div> </div>

View File

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

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useCallback } from 'react';
import { useCallback, useRef } from 'react'; import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
@@ -25,27 +25,13 @@ export function GlobalArticlesWidget({ params }: { params: Widget }) {
{ refetchOnWindowFocus: false } { 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 // render event match event kind
const renderItem = useCallback( const renderItem = useCallback(
(index: string | number) => { (event: NDKEvent) => {
const event: NDKEvent = data[index];
if (!event) return;
return ( return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}> <NoteWrapper key={event.id} event={event}>
<NoteWrapper event={event}> <ArticleNote />
<ArticleNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
}, },
[data] [data]
@@ -54,40 +40,32 @@ export function GlobalArticlesWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <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' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl"> <div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
) : items.length === 0 ? ( ) : data.length === 0 ? (
<div className="px-3 py-1.5"> <div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<p className="text-center text-sm font-medium text-white"> <img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
There have been no new articles in the last 24 hours. <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> </p>
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div <VList className="scrollbar-hide h-full">
style={{ {data.map((item) => renderItem(item))}
position: 'relative', <div className="h-16" />
width: '100%', </VList>
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>
)} )}
</div> </div>
</WidgetWrapper> </WidgetWrapper>

View File

@@ -1,7 +1,7 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useCallback } from 'react';
import { useCallback, useRef } from 'react'; import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
@@ -26,27 +26,13 @@ export function GlobalFilesWidget({ params }: { params: Widget }) {
{ refetchOnWindowFocus: false } { 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 // render event match event kind
const renderItem = useCallback( const renderItem = useCallback(
(index: string | number) => { (event: NDKEvent) => {
const event: NDKEvent = data[index];
if (!event) return;
return ( return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}> <NoteWrapper key={event.id} event={event}>
<NoteWrapper event={event}> <FileNote />
<FileNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
}, },
[data] [data]
@@ -55,40 +41,32 @@ export function GlobalFilesWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <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' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl"> <div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
) : items.length === 0 ? ( ) : data.length === 0 ? (
<div className="px-3 py-1.5"> <div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<p className="text-center text-sm font-medium text-white"> <img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
There have been no new files in the last 24 hours. <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> </p>
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div <VList className="scrollbar-hide h-full">
style={{ {data.map((item) => renderItem(item))}
position: 'relative', <div className="h-16" />
width: '100%', </VList>
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>
)} )}
</div> </div>
</WidgetWrapper> </WidgetWrapper>

View File

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

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useRef } from 'react'; import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider'; 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 ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[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 // render event match event kind
const renderItem = useCallback( const renderItem = useCallback(
(index: string | number) => { (dbEvent: DBEvent) => {
const event: NDKEvent = data[index]; const event: NDKEvent = JSON.parse(dbEvent.event as string);
if (!event) return;
return ( return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}> <NoteWrapper key={event.id} event={event}>
<NoteWrapper event={event}> <FileNote />
<FileNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
}, },
[data] [data]
@@ -56,75 +44,59 @@ export function LocalArticlesWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <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' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl"> <div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
) : items.length === 0 ? ( ) : dbEvents.length === 0 ? (
<div className="px-3 py-1.5"> <div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white"> <img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
There have been no new posts. <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> </p>
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div <VList className="scrollbar-hide h-full">
style={{ {dbEvents.map((item) => renderItem(item))}
position: 'relative', <div className="flex items-center justify-center px-3 py-1.5">
width: '100%', {dbEvents.length > 0 ? (
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button <button
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage} 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 ? ( {isFetchingNextPage ? (
<> <>
<span className="w-5" />
<span>Loading...</span> <span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" /> <LoaderIcon className="h-5 w-5 animate-spin text-white" />
</> </>
) : hasNextPage ? ( ) : hasNextPage ? (
<> <>
<span className="w-5" /> <ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span> <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> <span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</> </>
)} )}
</button> </button>
) : null}
</div> </div>
<div className="h-16" />
</VList>
)}
</div> </div>
</WidgetWrapper> </WidgetWrapper>
); );

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useRef } from 'react'; import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider'; 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 ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[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 // render event match event kind
const renderItem = useCallback( const renderItem = useCallback(
(index: string | number) => { (dbEvent: DBEvent) => {
const dbEvent: DBEvent = dbEvents[index];
if (!dbEvent) return;
const event: NDKEvent = JSON.parse(dbEvent.event as string); const event: NDKEvent = JSON.parse(dbEvent.event as string);
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return ( return (
<div <NoteWrapper
key={dbEvent.id + index} key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id}
data-index={index} event={event}
ref={virtualizer.measureElement} root={dbEvent.root_id}
reply={dbEvent.reply_id}
> >
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}> <TextNote />
<TextNote content={event.content} />
</NoteWrapper> </NoteWrapper>
</div>
); );
case NDKKind.Repost: case NDKKind.Repost:
return ( return <Repost key={dbEvent.id} event={event} />;
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={dbEvent.id} event={event} />
</div>
);
case 1063: case 1063:
return ( return (
<div <NoteWrapper key={dbEvent.id} event={event}>
key={dbEvent.id + index} <FileNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
case NDKKind.Article: case NDKKind.Article:
return ( return (
<div <NoteWrapper key={dbEvent.id} event={event}>
key={dbEvent.id + index} <ArticleNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
default: default:
return ( return (
<div <NoteWrapper key={dbEvent.id} event={event}>
key={dbEvent.id + index} <UnknownNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
} }
}, },
@@ -119,75 +81,59 @@ export function LocalFeedsWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <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' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl"> <div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
) : items.length === 0 ? ( ) : dbEvents.length === 0 ? (
<div className="px-3 py-1.5"> <div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white"> <img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
There have been no new posts. <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> </p>
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div <VList className="scrollbar-hide h-full">
style={{ {dbEvents.map((item) => renderItem(item))}
position: 'relative', <div className="flex items-center justify-center px-3 py-1.5">
width: '100%', {dbEvents.length > 0 ? (
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button <button
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage} 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 ? ( {isFetchingNextPage ? (
<> <>
<span className="w-5" />
<span>Loading...</span> <span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" /> <LoaderIcon className="h-5 w-5 animate-spin text-white" />
</> </>
) : hasNextPage ? ( ) : hasNextPage ? (
<> <>
<span className="w-5" /> <ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span> <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> <span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</> </>
)} )}
</button> </button>
) : null}
</div> </div>
<div className="h-16" />
</VList>
)}
</div> </div>
</WidgetWrapper> </WidgetWrapper>
); );

View File

@@ -1,7 +1,7 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useRef } from 'react'; import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider'; 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 ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[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 // render event match event kind
const renderItem = useCallback( const renderItem = useCallback(
(index: string | number) => { (dbEvent: DBEvent) => {
const event: NDKEvent = data[index]; const event: NDKEvent = JSON.parse(dbEvent.event as string);
if (!event) return;
return ( return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}> <NoteWrapper key={event.id} event={event}>
<NoteWrapper event={event}> <FileNote />
<FileNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
}, },
[data] [data]
@@ -56,75 +44,59 @@ export function LocalFilesWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title={params.title} /> <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' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl"> <div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
) : items.length === 0 ? ( ) : dbEvents.length === 0 ? (
<div className="px-3 py-1.5"> <div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white"> <img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
There have been no new posts. <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> </p>
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div <VList className="scrollbar-hide h-full">
style={{ {dbEvents.map((item) => renderItem(item))}
position: 'relative', <div className="flex items-center justify-center px-3 py-1.5">
width: '100%', {dbEvents.length > 0 ? (
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button <button
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage} 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 ? ( {isFetchingNextPage ? (
<> <>
<span className="w-5" />
<span>Loading...</span> <span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" /> <LoaderIcon className="h-5 w-5 animate-spin text-white" />
</> </>
) : hasNextPage ? ( ) : hasNextPage ? (
<> <>
<span className="w-5" /> <ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span> <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> <span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</> </>
)} )}
</button> </button>
) : null}
</div> </div>
<div className="h-16" />
</VList>
)}
</div> </div>
</WidgetWrapper> </WidgetWrapper>
); );

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useRef } from 'react'; import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider'; 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 ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[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 // render event match event kind
const renderItem = useCallback( const renderItem = useCallback(
(index: string | number) => { (dbEvent: DBEvent) => {
const dbEvent: DBEvent = dbEvents[index];
if (!dbEvent) return;
const event: NDKEvent = JSON.parse(dbEvent.event as string); const event: NDKEvent = JSON.parse(dbEvent.event as string);
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return ( return (
<div <NoteWrapper
key={dbEvent.id + index} key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id}
data-index={index} event={event}
ref={virtualizer.measureElement} root={dbEvent.root_id}
reply={dbEvent.reply_id}
> >
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}> <TextNote />
<TextNote content={event.content} />
</NoteWrapper> </NoteWrapper>
</div>
); );
case NDKKind.Repost: case NDKKind.Repost:
return ( return <Repost key={dbEvent.id} event={event} />;
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={dbEvent.id} event={event} />
</div>
);
case 1063: case 1063:
return ( return (
<div <NoteWrapper key={dbEvent.id} event={event}>
key={dbEvent.id + index} <FileNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
case NDKKind.Article: case NDKKind.Article:
return ( return (
<div <NoteWrapper key={dbEvent.id} event={event}>
key={dbEvent.id + index} <ArticleNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
default: default:
return ( return (
<div <NoteWrapper key={dbEvent.id} event={event}>
key={dbEvent.id + index} <UnknownNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
} }
}, },
@@ -118,7 +80,7 @@ export function LocalFollowsWidget({ params }: { params: Widget }) {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar id={params.id} title="Follows" /> <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' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl"> <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> </div>
) : ( ) : (
<div <VList className="scrollbar-hide h-full">
style={{ {dbEvents.map((item) => renderItem(item))}
position: 'relative', <div className="flex items-center justify-center px-3 py-1.5">
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">
{dbEvents.length > 0 ? ( {dbEvents.length > 0 ? (
<button <button
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage} 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 ? ( {isFetchingNextPage ? (
<> <>
<span className="w-5" />
<span>Loading...</span> <span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" /> <LoaderIcon className="h-5 w-5 animate-spin text-white" />
</> </>
) : hasNextPage ? ( ) : hasNextPage ? (
<> <>
<span className="w-5" /> <ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span> <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> <span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</> </>
)} )}
</button> </button>
) : null} ) : null}
</div> </div>
<div className="h-16" />
</VList>
)}
</div> </div>
</WidgetWrapper> </WidgetWrapper>
); );

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
@@ -38,80 +38,42 @@ export function LocalNetworkWidget() {
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []), () => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[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 // render event match event kind
const renderItem = useCallback( const renderItem = useCallback(
(index: string | number) => { (dbEvent: DBEvent) => {
const dbEvent: DBEvent = dbEvents[index];
if (!dbEvent) return;
const event: NDKEvent = JSON.parse(dbEvent.event as string); const event: NDKEvent = JSON.parse(dbEvent.event as string);
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return ( return (
<div <NoteWrapper
key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id + index} key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id}
data-index={index} event={event}
ref={virtualizer.measureElement} root={dbEvent.root_id}
reply={dbEvent.reply_id}
> >
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}> <TextNote />
<TextNote content={event.content} />
</NoteWrapper> </NoteWrapper>
</div>
); );
case NDKKind.Repost: case NDKKind.Repost:
return ( return <Repost key={dbEvent.id} event={event} />;
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={dbEvent.id} event={event} />
</div>
);
case 1063: case 1063:
return ( return (
<div <NoteWrapper key={dbEvent.id} event={event}>
key={dbEvent.id + index} <FileNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
case NDKKind.Article: case NDKKind.Article:
return ( return (
<div <NoteWrapper key={dbEvent.id} event={event}>
key={dbEvent.id + index} <ArticleNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
default: default:
return ( return (
<div <NoteWrapper key={dbEvent.id} event={event}>
key={dbEvent.id + index} <UnknownNote />
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper> </NoteWrapper>
</div>
); );
} }
}, },
@@ -125,7 +87,7 @@ export function LocalNetworkWidget() {
const filter: NDKFilter = { const filter: NDKFilter = {
kinds: [NDKKind.Text, NDKKind.Repost], kinds: [NDKKind.Text, NDKKind.Repost],
authors: db.account.network, 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) => { sub(filter, async (event) => {
@@ -138,7 +100,7 @@ export function LocalNetworkWidget() {
return ( return (
<WidgetWrapper> <WidgetWrapper>
<TitleBar title="👋 Network" /> <TitleBar title="👋 Network" />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20"> <div className="h-full">
{status === 'loading' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl"> <div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
@@ -160,59 +122,37 @@ export function LocalNetworkWidget() {
</div> </div>
</div> </div>
) : ( ) : (
<div <VList className="scrollbar-hide h-full">
style={{ {dbEvents.map((item) => renderItem(item))}
position: 'relative', <div className="flex items-center justify-center px-3 py-1.5">
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">
{dbEvents.length > 0 ? ( {dbEvents.length > 0 ? (
<button <button
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage} 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 ? ( {isFetchingNextPage ? (
<> <>
<span className="w-5" />
<span>Loading...</span> <span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" /> <LoaderIcon className="h-5 w-5 animate-spin text-white" />
</> </>
) : hasNextPage ? ( ) : hasNextPage ? (
<> <>
<span className="w-5" /> <ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span> <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> <span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</> </>
)} )}
</button> </button>
) : null} ) : null}
</div> </div>
<div className="h-16" />
</VList>
)}
</div> </div>
</WidgetWrapper> </WidgetWrapper>
); );

View File

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

View File

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