mirror of
https://github.com/lumehq/lume.git
synced 2025-04-07 03:18:12 +02:00
wip: refactor
This commit is contained in:
parent
ab61bfb2cd
commit
414dd50a5c
21
package.json
21
package.json
@ -20,6 +20,7 @@
|
||||
"@ctrl/magnet-link": "^3.1.2",
|
||||
"@headlessui/react": "^1.7.16",
|
||||
"@nostr-dev-kit/ndk": "^0.8.17",
|
||||
"@nostr-fetch/adapter-ndk": "^0.12.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
@ -44,21 +45,21 @@
|
||||
"@tauri-apps/plugin-stronghold": "github:tauri-apps/tauri-plugin-stronghold#v2",
|
||||
"@tauri-apps/plugin-upload": "github:tauri-apps/tauri-plugin-upload#v2",
|
||||
"@tauri-apps/plugin-window": "github:tauri-apps/tauri-plugin-window#v2",
|
||||
"@tiptap/extension-image": "^2.0.4",
|
||||
"@tiptap/extension-mention": "^2.0.4",
|
||||
"@tiptap/extension-placeholder": "^2.0.4",
|
||||
"@tiptap/pm": "^2.0.4",
|
||||
"@tiptap/react": "^2.0.4",
|
||||
"@tiptap/starter-kit": "^2.0.4",
|
||||
"@tiptap/suggestion": "^2.0.4",
|
||||
"@tiptap/extension-image": "^2.1.1",
|
||||
"@tiptap/extension-mention": "^2.1.1",
|
||||
"@tiptap/extension-placeholder": "^2.1.1",
|
||||
"@tiptap/pm": "^2.1.1",
|
||||
"@tiptap/react": "^2.1.1",
|
||||
"@tiptap/starter-kit": "^2.1.1",
|
||||
"@tiptap/suggestion": "^2.1.1",
|
||||
"@void-cat/api": "^1.0.7",
|
||||
"dayjs": "^1.11.9",
|
||||
"destr": "^2.0.1",
|
||||
"get-urls": "^12.1.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"immer": "^10.0.2",
|
||||
"light-bolt11-decoder": "^3.0.0",
|
||||
"lru-cache": "^10.0.1",
|
||||
"nostr-fetch": "^0.12.2",
|
||||
"nostr-tools": "^1.14.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
@ -68,7 +69,6 @@
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-player": "^2.12.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-virtuoso": "^4.5.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
@ -93,7 +93,7 @@
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^14.0.0",
|
||||
@ -105,7 +105,6 @@
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-top-level-await": "^1.3.1",
|
||||
"vite-tsconfig-paths": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
551
pnpm-lock.yaml
generated
551
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
20
src-tauri/Cargo.lock
generated
20
src-tauri/Cargo.lock
generated
@ -2912,9 +2912,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5381209b232fae437a2dd8054c9fe4953fb7a4b05e6e437f64aacdd01f97859"
|
||||
checksum = "a7de0cd2fdb9ef32781658513eab9080a22b3a9775955019daaf1f54bd37753a"
|
||||
dependencies = [
|
||||
"cocoa 0.25.0",
|
||||
"crossbeam-channel",
|
||||
@ -5589,9 +5589,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.31.0"
|
||||
version = "1.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40de3a2ba249dcb097e01be5e67a5ff53cf250397715a071a81543e8a832a920"
|
||||
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@ -5745,9 +5745,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tray-icon"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "672bc5a19944bfd51e5a874e9fcb7f129b76e5dd448fe295fa25411f3f800637"
|
||||
checksum = "6b164327e17101c78ba3dfdf879b977027ef1bd7855668ac30063de21fc02447"
|
||||
dependencies = [
|
||||
"cocoa 0.25.0",
|
||||
"core-graphics 0.23.1",
|
||||
@ -6492,9 +6492,9 @@ checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.11"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e461589e194280efaa97236b73623445efa195aa633fd7004f39805707a9d53"
|
||||
checksum = "83817bbecf72c73bad717ee86820ebf286203d2e04c3951f3cd538869c897364"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@ -6520,9 +6520,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.31.0"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6289018fa3cbc051c13f4ae1a102d80c3f35a527456c75567eb2cad6989020"
|
||||
checksum = "07bf838a5430184dfe0b1f568af7998a455c0df75a1df300a3894e0f181e7408"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"block",
|
||||
|
@ -16,7 +16,10 @@ tauri-build = { version = "2.0.0-alpha.8", features = [] }
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "2.0.0-alpha.11", features = ["macos-private-api"] }
|
||||
tauri = { version = "2.0.0-alpha.11", features = [
|
||||
"protocol-asset",
|
||||
"macos-private-api",
|
||||
] }
|
||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-stronghold = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
|
@ -1,9 +1,12 @@
|
||||
-- Add migration script here
|
||||
CREATE TABLE
|
||||
events (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
cache_key TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL UNIQUE,
|
||||
event_kind INTEGER NOT NULL DEFAULT 1,
|
||||
event TEXT NOT NULL
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
root_id TEXT,
|
||||
reply_id TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||
);
|
@ -0,0 +1,3 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE accounts
|
||||
ADD COLUMN last_login_at NUMBER NOT NULL DEFAULT 0;
|
@ -129,6 +129,12 @@ fn main() {
|
||||
sql: include_str!("../migrations/20230816090508_clean_up_tables.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 20230817014932,
|
||||
description: "add last login to account",
|
||||
sql: include_str!("../migrations/20230817014932_add_last_login_time_to_account.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
|
@ -80,7 +80,18 @@
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": "upgrade-insecure-requests"
|
||||
"csp": {
|
||||
"connect-src": "ipc: https://ipc.localhost",
|
||||
"content-security-policy": "upgrade-insecure-requests"
|
||||
},
|
||||
"freezePrototype": false,
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": {
|
||||
"allow": ["$APPCONFIG/*.db", "$RESOURCE/**"],
|
||||
"deny": ["$APPCONFIG/*.stronghold"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { BaseDirectory, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@ -16,7 +15,6 @@ import { useStronghold } from '@stores/stronghold';
|
||||
export function CreateStep1Screen() {
|
||||
const { db } = useStorage();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
const setTempPrivkey = useOnboarding((state) => state.setTempPrivkey);
|
||||
@ -52,33 +50,16 @@ export function CreateStep1Screen() {
|
||||
setDownloaded(true);
|
||||
};
|
||||
|
||||
const account = useMutation({
|
||||
mutationFn: (data: {
|
||||
npub: string;
|
||||
pubkey: string;
|
||||
follows: null | string[][];
|
||||
is_active: number;
|
||||
}) => {
|
||||
return db.createAccount(data.npub, data.pubkey);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(['account'], data);
|
||||
},
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
setLoading(true);
|
||||
|
||||
// update state
|
||||
setPrivkey(privkey);
|
||||
setTempPrivkey(privkey); // only use if user close app and reopen it
|
||||
setPubkey(pubkey);
|
||||
|
||||
account.mutate({
|
||||
npub,
|
||||
pubkey,
|
||||
follows: null,
|
||||
is_active: 1,
|
||||
});
|
||||
// save to database
|
||||
db.createAccount(npub, pubkey);
|
||||
|
||||
// redirect to next step
|
||||
navigate('/auth/create/step-2', { replace: true });
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@ -17,7 +16,6 @@ import { useNostr } from '@utils/hooks/useNostr';
|
||||
export function CreateStep3Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [picture, setPicture] = useState(DEFAULT_AVATAR);
|
||||
@ -47,8 +45,6 @@ export function CreateStep3Screen() {
|
||||
tags: [],
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries(['account']);
|
||||
|
||||
if (event) {
|
||||
navigate('/auth/onboarding', { replace: true });
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
@ -31,9 +30,6 @@ const resolver: Resolver<FormValues> = async (values) => {
|
||||
};
|
||||
|
||||
export function ImportStep1Screen() {
|
||||
const { db } = useStorage();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
const setTempPubkey = useOnboarding((state) => state.setTempPrivkey);
|
||||
@ -42,20 +38,7 @@ export function ImportStep1Screen() {
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const account = useMutation({
|
||||
mutationFn: (data: {
|
||||
npub: string;
|
||||
pubkey: string;
|
||||
follows: null | string[];
|
||||
is_active: number | boolean;
|
||||
}) => {
|
||||
return db.createAccount(data.npub, data.pubkey);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(['account'], data);
|
||||
},
|
||||
});
|
||||
|
||||
const { db } = useStorage();
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
@ -81,12 +64,7 @@ export function ImportStep1Screen() {
|
||||
setPubkey(pubkey);
|
||||
|
||||
// add account to local database
|
||||
account.mutate({
|
||||
npub,
|
||||
pubkey,
|
||||
follows: null,
|
||||
is_active: 1,
|
||||
});
|
||||
db.createAccount(npub, pubkey);
|
||||
|
||||
// redirect to step 2
|
||||
navigate('/auth/import/step-2', { replace: true });
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@ -13,7 +12,6 @@ import { useOnboarding } from '@stores/onboarding';
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function ImportStep3Screen() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
@ -30,10 +28,6 @@ export function ImportStep3Screen() {
|
||||
const data = await fetchUserData();
|
||||
|
||||
if (data.status === 'ok') {
|
||||
// update last login
|
||||
await db.updateLastLogin(Math.floor(Date.now() / 1000));
|
||||
|
||||
queryClient.invalidateQueries(['account']);
|
||||
navigate('/auth/onboarding/step-2', { replace: true });
|
||||
} else {
|
||||
console.log('error: ', data.message);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
@ -14,12 +14,11 @@ import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { arrayToNIP02 } from '@utils/transform';
|
||||
|
||||
export function OnboardStep1Screen() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
const { publish, fetchUserData, prefetchEvents } = useNostr();
|
||||
const { db } = useStorage();
|
||||
const { publish, fetchUserData } = useNostr();
|
||||
const { status, data } = useQuery(['trending-profiles'], async () => {
|
||||
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
|
||||
if (!res.ok) {
|
||||
@ -45,14 +44,13 @@ export function OnboardStep1Screen() {
|
||||
|
||||
const tags = arrayToNIP02([...follows, db.account.pubkey]);
|
||||
const event = await publish({ content: '', kind: 3, tags: tags });
|
||||
await db.updateAccount('follows', follows);
|
||||
|
||||
// prefetch notes with current follows
|
||||
const data = await fetchUserData(follows);
|
||||
// prefetch data
|
||||
const user = await fetchUserData(follows);
|
||||
const data = await prefetchEvents();
|
||||
|
||||
// redirect to next step
|
||||
if (event && data.status === 'ok') {
|
||||
queryClient.invalidateQueries(['account']);
|
||||
if (event && user.status === 'ok' && data.status === 'ok') {
|
||||
navigate('/auth/onboarding/step-2', { replace: true });
|
||||
} else {
|
||||
setLoading(false);
|
||||
|
@ -1,92 +1,112 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import { NoteKind_1, NoteKind_1063, NoteThread, Repost } from '@shared/notes';
|
||||
import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport';
|
||||
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||
import { TitleBar } from '@shared/titleBar';
|
||||
|
||||
import { LumeEvent, Widget } from '@utils/types';
|
||||
import { DBEvent, Widget } from '@utils/types';
|
||||
|
||||
export function FeedBlock({ params }: { params: Widget }) {
|
||||
const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
export function FeedWidget({ params }: { params: Widget }) {
|
||||
const { db } = useStorage();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['newsfeed', params.content],
|
||||
queryFn: async () => {
|
||||
return { data: [], nextCursor: 0 };
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
const authors = JSON.parse(params.content);
|
||||
return await db.getAllEventsByAuthors(authors, 20, pageParam);
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
});
|
||||
|
||||
const notes = data ? data.pages.flatMap((d: { data: LumeEvent[] }) => d.data) : [];
|
||||
|
||||
const parentRef = useRef();
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: hasNextPage ? notes.length + 1 : notes.length,
|
||||
const dbEvents = useMemo(
|
||||
() => (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: () => 500,
|
||||
overscan: 2,
|
||||
estimateSize: () => 650,
|
||||
overscan: 4,
|
||||
});
|
||||
const items = virtualizer.getVirtualItems();
|
||||
|
||||
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
|
||||
const totalSize = rowVirtualizer.getTotalSize();
|
||||
|
||||
useEffect(() => {
|
||||
const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse();
|
||||
|
||||
if (!lastItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastItem.index >= notes.length - 1 && hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
||||
|
||||
// render event match event kind
|
||||
const renderItem = useCallback(
|
||||
(index: string | number) => {
|
||||
const note: LumeEvent = notes[index];
|
||||
if (!note) return;
|
||||
switch (note.kind) {
|
||||
const dbEvent: DBEvent = dbEvents[index];
|
||||
if (!dbEvent) return;
|
||||
|
||||
const event: NDKEvent = JSON.parse(dbEvent.event as string);
|
||||
|
||||
switch (event.kind) {
|
||||
case 1: {
|
||||
const root = note.tags.find((el) => el[3] === 'root')?.[1];
|
||||
const reply = note.tags.find((el) => el[3] === 'reply')?.[1];
|
||||
if (root || reply) {
|
||||
if (dbEvent.root_id || dbEvent.reply_id) {
|
||||
return (
|
||||
<div key={note.id} data-index={index} ref={rowVirtualizer.measureElement}>
|
||||
<NoteThread event={note} root={root} reply={reply} />
|
||||
<div
|
||||
key={(dbEvent.root_id || dbEvent.reply_id) + dbEvent.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteThread
|
||||
event={event}
|
||||
root={dbEvent.root_id}
|
||||
reply={dbEvent.reply_id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={note.id} data-index={index} ref={rowVirtualizer.measureElement}>
|
||||
<NoteKind_1 event={note} skipMetadata={false} />
|
||||
<div
|
||||
key={dbEvent.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteKind_1 event={event} skipMetadata={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
case 6:
|
||||
return (
|
||||
<div key={note.id} data-index={index} ref={rowVirtualizer.measureElement}>
|
||||
<Repost key={note.id} event={note} />
|
||||
<div
|
||||
key={dbEvent.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<Repost key={dbEvent.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
case 1063:
|
||||
return (
|
||||
<div key={note.id} data-index={index} ref={rowVirtualizer.measureElement}>
|
||||
<NoteKind_1063 key={note.id} event={note} />
|
||||
<div
|
||||
key={dbEvent.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteKind_1063 key={dbEvent.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div key={note.id} data-index={index} ref={rowVirtualizer.measureElement}>
|
||||
<NoteKindUnsupport key={note.id} event={note} />
|
||||
<div
|
||||
key={dbEvent.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteKindUnsupport key={dbEvent.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
[notes]
|
||||
[dbEvents]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -99,32 +119,31 @@ export function FeedBlock({ params }: { params: Widget }) {
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
) : itemsVirtualizer.length === 0 ? (
|
||||
) : items.length === 0 ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="bbg-white/10 rounded-xl px-3 py-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p className="text-center text-sm text-white">
|
||||
Not found any posts from last 48 hours
|
||||
Not found any postrs from last 48 hours
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: `${totalSize}px`,
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
transform: `translateY(${
|
||||
itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin
|
||||
}px)`,
|
||||
transform: `translateY(${items[0].start}px)`,
|
||||
}}
|
||||
>
|
||||
{itemsVirtualizer.map((virtualRow) => renderItem(virtualRow.index))}
|
||||
{items.map((item) => renderItem(item.index))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -135,6 +154,33 @@ export function FeedBlock({ params }: { params: Widget }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-3 py-1.5">
|
||||
<button
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Loading...</span>
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</>
|
||||
) : hasNextPage ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Load more</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Nothing more to load</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,35 +1,74 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
|
||||
import { NoteKind_1, NoteSkeleton } from '@shared/notes';
|
||||
import { NoteKind_1, NoteSkeleton, Repost } from '@shared/notes';
|
||||
import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport';
|
||||
import { TitleBar } from '@shared/titleBar';
|
||||
|
||||
import { nHoursAgo } from '@utils/date';
|
||||
import { LumeEvent, Widget } from '@utils/types';
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function HashtagBlock({ params }: { params: Widget }) {
|
||||
export function HashtagWidget({ params }: { params: Widget }) {
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(['hashtag', params.content], async () => {
|
||||
const { status, data } = useQuery(['hashtag-widget', params.content], async () => {
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
'#t': [params.content],
|
||||
since: nHoursAgo(24),
|
||||
});
|
||||
return [...events] as unknown as LumeEvent[];
|
||||
return [...events] as unknown as NDKEvent[];
|
||||
});
|
||||
|
||||
const parentRef = useRef();
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const virtualizer = useVirtualizer({
|
||||
count: data ? data.length : 0,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 400,
|
||||
estimateSize: () => 650,
|
||||
overscan: 4,
|
||||
});
|
||||
const items = virtualizer.getVirtualItems();
|
||||
|
||||
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
|
||||
const totalSize = rowVirtualizer.getTotalSize();
|
||||
// render event match event kind
|
||||
const renderItem = useCallback(
|
||||
(index: string | number) => {
|
||||
const event: NDKEvent = data[index];
|
||||
if (!event) return;
|
||||
|
||||
switch (event.kind) {
|
||||
case 1:
|
||||
return (
|
||||
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
|
||||
<NoteKind_1 event={event} skipMetadata={false} />
|
||||
</div>
|
||||
);
|
||||
case 6:
|
||||
return (
|
||||
<div
|
||||
key={event.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<Repost key={event.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div
|
||||
key={event.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteKindUnsupport key={event.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative w-[400px] shrink-0 bg-white/10">
|
||||
@ -41,40 +80,31 @@ export function HashtagBlock({ params }: { params: Widget }) {
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
) : itemsVirtualizer.length === 0 ? (
|
||||
) : items.length === 0 ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl bg-white/10 px-3 py-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p className="text-center text-sm font-medium text-white">
|
||||
No new posts about this hashtag in 24 hours ago
|
||||
No new postrs about this hashtag in 24 hours ago
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: `${totalSize}px`,
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
transform: `translateY(${
|
||||
itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin
|
||||
}px)`,
|
||||
transform: `translateY(${items[0].start}px)`,
|
||||
}}
|
||||
>
|
||||
{itemsVirtualizer.map((virtualRow) => (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
data-index={virtualRow.index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
>
|
||||
<NoteKind_1 event={data[virtualRow.index]} />
|
||||
</div>
|
||||
))}
|
||||
{items.map((item) => renderItem(item.index))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NDKFilter } from '@nostr-dev-kit/ndk';
|
||||
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
@ -6,85 +6,75 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import { NoteKind_1, NoteKind_1063, NoteThread, Repost } from '@shared/notes';
|
||||
import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport';
|
||||
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||
import { TitleBar } from '@shared/titleBar';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
import { DBEvent } from '@utils/types';
|
||||
|
||||
export function NetworkBlock() {
|
||||
export function NetworkWidget() {
|
||||
const { sub } = useNostr();
|
||||
const { db } = useStorage();
|
||||
const { sub, fetchNotes } = useNostr();
|
||||
const { status, data, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
|
||||
queryKey: ['network-widget'],
|
||||
queryFn: async ({ pageParam = 24 }) => {
|
||||
return { data: [], nextCursor: 0 };
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['network-widget'],
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
return await db.getAllEvents(20, pageParam);
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const parentRef = useRef();
|
||||
const notes = useMemo(
|
||||
() => (data ? data.pages.flatMap((d: { data: LumeEvent[] }) => d.data) : []),
|
||||
const dbEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
|
||||
[data]
|
||||
);
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: hasNextPage ? notes.length + 1 : notes.length,
|
||||
const parentRef = useRef<HTMLDivElement>();
|
||||
const virtualizer = useVirtualizer({
|
||||
count: hasNextPage ? dbEvents.length : dbEvents.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 500,
|
||||
overscan: 2,
|
||||
estimateSize: () => 650,
|
||||
overscan: 4,
|
||||
});
|
||||
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
|
||||
const totalSize = rowVirtualizer.getTotalSize();
|
||||
|
||||
useEffect(() => {
|
||||
const since = Math.floor(Date.now() / 1000);
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1, 6],
|
||||
authors: db.account.network,
|
||||
since: since,
|
||||
};
|
||||
|
||||
sub(filter, (event) => console.log('[network] event received: ', event));
|
||||
}, []);
|
||||
const items = virtualizer.getVirtualItems();
|
||||
|
||||
// render event match event kind
|
||||
const renderItem = useCallback(
|
||||
(index: string | number) => {
|
||||
const note: LumeEvent = notes[index];
|
||||
if (!note) return;
|
||||
switch (note.kind) {
|
||||
const dbEvent: DBEvent = dbEvents[index];
|
||||
if (!dbEvent) return;
|
||||
|
||||
const event: NDKEvent = JSON.parse(dbEvent.event as string);
|
||||
|
||||
switch (event.kind) {
|
||||
case 1: {
|
||||
let root: string;
|
||||
let reply: string;
|
||||
if (note.tags?.[0]?.[0] === 'e' && !note.tags?.[0]?.[3]) {
|
||||
root = note.tags[0][1];
|
||||
} else {
|
||||
root = note.tags.find((el) => el[3] === 'root')?.[1];
|
||||
reply = note.tags.find((el) => el[3] === 'reply')?.[1];
|
||||
}
|
||||
if (root || reply) {
|
||||
if (dbEvent.root_id || dbEvent.reply_id) {
|
||||
return (
|
||||
<div
|
||||
key={(root || reply) + note.id + index}
|
||||
key={(dbEvent.root_id || dbEvent.reply_id) + dbEvent.id + index}
|
||||
data-index={index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteThread event={note} root={root} reply={reply} />
|
||||
<NoteThread
|
||||
event={event}
|
||||
root={dbEvent.root_id}
|
||||
reply={dbEvent.reply_id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
key={note.id + index}
|
||||
key={dbEvent.id + index}
|
||||
data-index={index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteKind_1 event={note} skipMetadata={false} />
|
||||
<NoteKind_1 event={event} skipMetadata={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -92,38 +82,52 @@ export function NetworkBlock() {
|
||||
case 6:
|
||||
return (
|
||||
<div
|
||||
key={note.id + index}
|
||||
key={dbEvent.id + index}
|
||||
data-index={index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<Repost key={note.id} event={note} />
|
||||
<Repost key={dbEvent.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
case 1063:
|
||||
return (
|
||||
<div
|
||||
key={note.id + index}
|
||||
key={dbEvent.id + index}
|
||||
data-index={index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteKind_1063 key={note.id} event={note} />
|
||||
<NoteKind_1063 key={dbEvent.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div
|
||||
key={note.id + index}
|
||||
key={dbEvent.id + index}
|
||||
data-index={index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteKindUnsupport key={note.id} event={note} />
|
||||
<NoteKindUnsupport key={dbEvent.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
[notes]
|
||||
[dbEvents]
|
||||
);
|
||||
|
||||
// subscribe for new event
|
||||
// sub will be managed by lru-cache
|
||||
useEffect(() => {
|
||||
if (db.account && db.account.network) {
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1, 6],
|
||||
authors: db.account.network,
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
sub(filter, (event) => console.log('[network] event received: ', event.content));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative w-[400px] shrink-0 bg-white/10">
|
||||
<TitleBar title="Network" />
|
||||
@ -134,7 +138,7 @@ export function NetworkBlock() {
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
) : itemsVirtualizer.length === 0 ? (
|
||||
) : dbEvents.length === 0 ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl bg-white/10 px-3 py-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
@ -154,30 +158,56 @@ export function NetworkBlock() {
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: `${totalSize}px`,
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
transform: `translateY(${
|
||||
itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin
|
||||
}px)`,
|
||||
transform: `translateY(${items[0].start}px)`,
|
||||
}}
|
||||
>
|
||||
{itemsVirtualizer.map((virtualRow) => renderItem(virtualRow.index))}
|
||||
{items.map((item) => renderItem(item.index))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isFetchingNextPage && (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="mb-20 px-3 py-1.5">
|
||||
<div className="rounded-xl bg-white/10 px-3 py-3">
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-3 py-1.5">
|
||||
<button
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Loading...</span>
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</>
|
||||
) : hasNextPage ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Load more</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Nothing more to load</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,37 +1,79 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
|
||||
import { NoteKind_1, NoteSkeleton } from '@shared/notes';
|
||||
import { NoteKind_1, NoteSkeleton, Repost } from '@shared/notes';
|
||||
import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport';
|
||||
import { TitleBar } from '@shared/titleBar';
|
||||
import { UserProfile } from '@shared/userProfile';
|
||||
|
||||
import { nHoursAgo } from '@utils/date';
|
||||
import { LumeEvent, Widget } from '@utils/types';
|
||||
|
||||
export function UserBlock({ params }: { params: Widget }) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
import { DBEvent, Widget } from '@utils/types';
|
||||
|
||||
export function UserWidget({ params }: { params: Widget }) {
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(['user-feed', params.content], async () => {
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
authors: [params.content],
|
||||
since: nHoursAgo(48),
|
||||
});
|
||||
return [...events] as unknown as LumeEvent[];
|
||||
});
|
||||
const { status, data } = useQuery(
|
||||
['user-widget', params.content],
|
||||
async () => {
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
authors: [params.content],
|
||||
since: nHoursAgo(24),
|
||||
});
|
||||
return [...events] as unknown as DBEvent[];
|
||||
},
|
||||
{ refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const virtualizer = useVirtualizer({
|
||||
count: data ? data.length : 0,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 400,
|
||||
overscan: 2,
|
||||
estimateSize: () => 650,
|
||||
overscan: 4,
|
||||
});
|
||||
const items = virtualizer.getVirtualItems();
|
||||
|
||||
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
|
||||
// render event match event kind
|
||||
const renderItem = useCallback(
|
||||
(index: string | number) => {
|
||||
const event: NDKEvent = data[index];
|
||||
if (!event) return;
|
||||
|
||||
switch (event.kind) {
|
||||
case 1:
|
||||
return (
|
||||
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
|
||||
<NoteKind_1 event={event} skipMetadata={false} />
|
||||
</div>
|
||||
);
|
||||
case 6:
|
||||
return (
|
||||
<div
|
||||
key={event.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<Repost key={event.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div
|
||||
key={event.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteKindUnsupport key={event.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative w-[400px] shrink-0 bg-white/10">
|
||||
@ -49,41 +91,31 @@ export function UserBlock({ params }: { params: Widget }) {
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
) : itemsVirtualizer.length === 0 ? (
|
||||
) : items.length === 0 ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl bg-white/10 px-3 py-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p className="text-center text-sm text-white">
|
||||
No new posts from this user in 48 hours ago
|
||||
No new postr from user in 24 hours ago
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
transform: `translateY(${
|
||||
itemsVirtualizer[0].start - rowVirtualizer.options.scrollMargin
|
||||
}px)`,
|
||||
transform: `translateY(${items[0].start}px)`,
|
||||
}}
|
||||
>
|
||||
{itemsVirtualizer.map((virtualRow) => (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
data-index={virtualRow.index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
>
|
||||
<NoteKind_1 event={data[virtualRow.index]} />
|
||||
</div>
|
||||
))}
|
||||
<div className="h-20" />
|
||||
{items.map((item) => renderItem(item.index))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -3,11 +3,11 @@ import { useCallback, useEffect } from 'react';
|
||||
import { FeedModal } from '@app/space/components/modals/feed';
|
||||
import { HashtagModal } from '@app/space/components/modals/hashtag';
|
||||
import { ImageModal } from '@app/space/components/modals/image';
|
||||
import { FeedBlock } from '@app/space/components/widgets/feed';
|
||||
import { HashtagBlock } from '@app/space/components/widgets/hashtag';
|
||||
import { NetworkBlock } from '@app/space/components/widgets/network';
|
||||
import { FeedWidget } from '@app/space/components/widgets/feed';
|
||||
import { HashtagWidget } from '@app/space/components/widgets/hashtag';
|
||||
import { NetworkWidget } from '@app/space/components/widgets/network';
|
||||
import { ThreadBlock } from '@app/space/components/widgets/thread';
|
||||
import { UserBlock } from '@app/space/components/widgets/user';
|
||||
import { UserWidget } from '@app/space/components/widgets/user';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
@ -29,15 +29,15 @@ export function SpaceScreen() {
|
||||
(widget: Widget) => {
|
||||
switch (widget.kind) {
|
||||
case 1:
|
||||
return <FeedBlock key={widget.id} params={widget} />;
|
||||
return <FeedWidget key={widget.id} params={widget} />;
|
||||
case 2:
|
||||
return <ThreadBlock key={widget.id} params={widget} />;
|
||||
case 3:
|
||||
return <HashtagBlock key={widget.id} params={widget} />;
|
||||
return <HashtagWidget key={widget.id} params={widget} />;
|
||||
case 5:
|
||||
return <UserBlock key={widget.id} params={widget} />;
|
||||
return <UserWidget key={widget.id} params={widget} />;
|
||||
case 9999:
|
||||
return <NetworkBlock key={widget.id} />;
|
||||
return <NetworkWidget key={widget.id} />;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -52,7 +52,7 @@ export function SpaceScreen() {
|
||||
return (
|
||||
<div className="scrollbar-hide flex h-full w-full flex-nowrap divide-x divide-white/5 overflow-x-auto overflow-y-hidden">
|
||||
{!widgets ? (
|
||||
<div className="flex w-[350px] shrink-0 flex-col">
|
||||
<div className="flex w-[400px] shrink-0 flex-col">
|
||||
<div className="flex w-full flex-1 items-center justify-center p-3">
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white/10" />
|
||||
</div>
|
||||
@ -60,14 +60,14 @@ export function SpaceScreen() {
|
||||
) : (
|
||||
widgets.map((widget) => renderItem(widget))
|
||||
)}
|
||||
<div className="flex w-[350px] shrink-0 flex-col">
|
||||
<div className="flex w-[250px] shrink-0 flex-col">
|
||||
<div className="inline-flex h-full w-full flex-col items-center justify-center gap-1">
|
||||
<FeedModal />
|
||||
<ImageModal />
|
||||
<HashtagModal />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[250px] shrink-0" />
|
||||
<div className="w-[150px] shrink-0" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { useNostr } from '@utils/hooks/useNostr';
|
||||
export function SplashScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ndk, relayUrls } = useNDK();
|
||||
const { fetchUserData } = useNostr();
|
||||
const { fetchUserData, prefetchEvents } = useNostr();
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
||||
@ -27,9 +27,10 @@ export function SplashScreen() {
|
||||
|
||||
try {
|
||||
const user = await fetchUserData();
|
||||
if (user.status === 'ok') {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
await db.updateLastLogin(now);
|
||||
const data = await prefetchEvents();
|
||||
|
||||
if (user.status === 'ok' && data.status === 'ok') {
|
||||
await db.updateLastLogin();
|
||||
await invoke('close_splashscreen');
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { NDKCacheAdapter } from '@nostr-dev-kit/ndk';
|
||||
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
|
||||
import { LumeStorage } from '@libs/storage/instance';
|
||||
import { Store } from '@tauri-apps/plugin-store';
|
||||
|
||||
export default class TauriAdapter implements NDKCacheAdapter {
|
||||
public store: LumeStorage;
|
||||
public store: Store;
|
||||
readonly locking: boolean;
|
||||
|
||||
constructor(db: LumeStorage) {
|
||||
this.store = db;
|
||||
constructor() {
|
||||
this.store = new Store('.ndk_cache.dat');
|
||||
this.locking = true;
|
||||
}
|
||||
|
||||
@ -21,34 +20,15 @@ export default class TauriAdapter implements NDKCacheAdapter {
|
||||
for (const author of filter.authors) {
|
||||
for (const kind of filter.kinds) {
|
||||
const key = `${author}:${kind}`;
|
||||
promises.concat(this.store.getALlEventByKey(key));
|
||||
promises.push(this.store.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
for (const result of results) {
|
||||
if (result && result.event) {
|
||||
console.log('cache hit: ', result.event);
|
||||
const ndkEvent = new NDKEvent(
|
||||
subscription.ndk,
|
||||
JSON.parse(result.event as string)
|
||||
);
|
||||
subscription.eventReceived(ndkEvent, undefined, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.ids) {
|
||||
for (const id of filter.ids) {
|
||||
const cacheEvent = await this.store.getEventByID(id);
|
||||
|
||||
if (cacheEvent) {
|
||||
console.log('cache hit: ', id);
|
||||
const ndkEvent = new NDKEvent(
|
||||
subscription.ndk,
|
||||
JSON.parse(cacheEvent.event as string)
|
||||
);
|
||||
if (result) {
|
||||
const ndkEvent = new NDKEvent(subscription.ndk, JSON.parse(result as string));
|
||||
subscription.eventReceived(ndkEvent, undefined, true);
|
||||
}
|
||||
}
|
||||
@ -60,9 +40,13 @@ export default class TauriAdapter implements NDKCacheAdapter {
|
||||
const key = `${nostrEvent.pubkey}:${nostrEvent.kind}`;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Promise.all([
|
||||
this.store.createEvent(key, event.id, event.kind, JSON.stringify(nostrEvent)),
|
||||
]).then(() => resolve());
|
||||
Promise.all([this.store.set(key, JSON.stringify(nostrEvent))]).then(() =>
|
||||
resolve()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async saveCache(): Promise<void> {
|
||||
return await this.store.save();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// inspire by: https://github.com/nostr-dev-kit/ndk-react/
|
||||
import NDK from '@nostr-dev-kit/ndk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import TauriAdapter from '@libs/ndk/cache';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
@ -13,6 +13,8 @@ export const NDKInstance = () => {
|
||||
const [ndk, setNDK] = useState<NDK | undefined>(undefined);
|
||||
const [relayUrls, setRelayUrls] = useState<string[]>([]);
|
||||
|
||||
const cacheAdapter = useMemo(() => new TauriAdapter(), [ndk]);
|
||||
|
||||
// TODO: fully support NIP-11
|
||||
async function verifyRelays(relays: string[]) {
|
||||
const verifiedRelays: string[] = [];
|
||||
@ -64,7 +66,6 @@ export const NDKInstance = () => {
|
||||
explicitRelayUrls = await verifyRelays(FULL_RELAYS);
|
||||
}
|
||||
|
||||
const cacheAdapter = new TauriAdapter(db);
|
||||
console.log('ndk cache adapter: ', cacheAdapter);
|
||||
const instance = new NDK({ explicitRelayUrls, cacheAdapter });
|
||||
|
||||
@ -80,6 +81,10 @@ export const NDKInstance = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!ndk) initNDK();
|
||||
|
||||
return () => {
|
||||
cacheAdapter.saveCache();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
@ -2,7 +2,7 @@ import { BaseDirectory, removeFile } from '@tauri-apps/plugin-fs';
|
||||
import Database from '@tauri-apps/plugin-sql';
|
||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
||||
|
||||
import { Account, LumeEvent, Relays, Widget } from '@utils/types';
|
||||
import { Account, DBEvent, Relays, Widget } from '@utils/types';
|
||||
|
||||
export class LumeStorage {
|
||||
public db: Database;
|
||||
@ -93,6 +93,13 @@ export class LumeStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public async updateLastLogin() {
|
||||
return await this.db.execute(
|
||||
'UPDATE accounts SET last_login_at = $1 WHERE id = $2;',
|
||||
[Math.floor(Date.now() / 1000), this.account.id]
|
||||
);
|
||||
}
|
||||
|
||||
public async getWidgets() {
|
||||
const result: Array<Widget> = await this.db.select(
|
||||
`SELECT * FROM widgets WHERE account_id = "${this.account.id}" ORDER BY created_at DESC;`
|
||||
@ -122,35 +129,99 @@ export class LumeStorage {
|
||||
}
|
||||
|
||||
public async createEvent(
|
||||
cacheKey: string,
|
||||
event_id: string,
|
||||
event_kind: number,
|
||||
event: string
|
||||
id: string,
|
||||
event: string,
|
||||
author: string,
|
||||
root_id: string,
|
||||
reply_id: string,
|
||||
created_at: number
|
||||
) {
|
||||
return await this.db.execute(
|
||||
'INSERT OR IGNORE INTO events (cache_key, event_id, event_kind, event) VALUES ($1, $2, $3, $4);',
|
||||
[cacheKey, event_id, event_kind, event]
|
||||
'INSERT OR IGNORE INTO events (id, account_id, event, author, root_id, reply_id, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7);',
|
||||
[id, this.account.id, event, author, root_id, reply_id, created_at]
|
||||
);
|
||||
}
|
||||
|
||||
public async getALlEventByKey(cacheKey: string) {
|
||||
const events: LumeEvent[] = await this.db.select(
|
||||
'SELECT * FROM events WHERE cache_key = $1 ORDER BY id DESC;',
|
||||
[cacheKey]
|
||||
);
|
||||
|
||||
if (events.length < 1) return null;
|
||||
return events;
|
||||
}
|
||||
|
||||
public async getEventByID(id: string) {
|
||||
const event = await this.db.select(
|
||||
'SELECT * FROM events WHERE event_id = $1 ORDER BY id DESC LIMIT 1;',
|
||||
const results: DBEvent[] = await this.db.select(
|
||||
'SELECT * FROM events WHERE id = $1 ORDER BY id DESC LIMIT 1;',
|
||||
[id]
|
||||
)?.[0];
|
||||
);
|
||||
|
||||
if (!event) return null;
|
||||
return event;
|
||||
if (results.length < 1) return null;
|
||||
return results[0];
|
||||
}
|
||||
|
||||
public async countTotalEvents() {
|
||||
const result: Array<{ total: string }> = await this.db.select(
|
||||
'SELECT COUNT(*) AS "total" FROM events;'
|
||||
);
|
||||
return parseInt(result[0].total);
|
||||
}
|
||||
|
||||
public async getAllEvents(limit: number, offset: number) {
|
||||
const totalEvents = await this.countTotalEvents();
|
||||
const nextCursor = offset + limit;
|
||||
|
||||
const events: { data: DBEvent[] | null; nextCursor: number } = {
|
||||
data: null,
|
||||
nextCursor: 0,
|
||||
};
|
||||
|
||||
const query: DBEvent[] = await this.db.select(
|
||||
'SELECT * FROM events ORDER BY created_at DESC LIMIT $1 OFFSET $2;',
|
||||
[limit, offset]
|
||||
);
|
||||
|
||||
if (query && query.length > 0) {
|
||||
events['data'] = query;
|
||||
events['nextCursor'] =
|
||||
Math.round(totalEvents / nextCursor) > 1 ? nextCursor : undefined;
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
return {
|
||||
data: [],
|
||||
nextCursor: 0,
|
||||
};
|
||||
}
|
||||
|
||||
public async getAllEventsByAuthors(authors: string[], limit: number, offset: number) {
|
||||
const totalEvents = await this.countTotalEvents();
|
||||
const nextCursor = offset + limit;
|
||||
const authorsArr = `'${authors.join("','")}'`;
|
||||
|
||||
const events: { data: DBEvent[] | null; nextCursor: number } = {
|
||||
data: null,
|
||||
nextCursor: 0,
|
||||
};
|
||||
|
||||
const query: DBEvent[] = await this.db.select(
|
||||
'SELECT * FROM events WHERE author IN ($1) ORDER BY created_at DESC LIMIT $2 OFFSET $3;',
|
||||
[authorsArr, limit, offset]
|
||||
);
|
||||
|
||||
if (query && query.length > 0) {
|
||||
events['data'] = query;
|
||||
events['nextCursor'] =
|
||||
Math.round(totalEvents / nextCursor) > 1 ? nextCursor : undefined;
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
return {
|
||||
data: [],
|
||||
nextCursor: 0,
|
||||
};
|
||||
}
|
||||
|
||||
public async isEventsEmpty() {
|
||||
const results: DBEvent[] = await this.db.select(
|
||||
'SELECT * FROM events ORDER BY id DESC LIMIT 1;'
|
||||
);
|
||||
|
||||
return results.length < 1;
|
||||
}
|
||||
|
||||
public async getExplicitRelayUrls() {
|
||||
@ -175,13 +246,6 @@ export class LumeStorage {
|
||||
return await this.db.execute(`DELETE FROM relays WHERE relay = "${relay}";`);
|
||||
}
|
||||
|
||||
public async updateLastLogin(time: number) {
|
||||
return await this.db.execute(
|
||||
'UPDATE settings SET value = $1 WHERE key = "last_login";',
|
||||
[time]
|
||||
);
|
||||
}
|
||||
|
||||
public async removePrivkey() {
|
||||
return await this.db.execute(
|
||||
`UPDATE accounts SET privkey = "privkey is stored in secure storage" WHERE id = "${this.account.id}";`
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { NoteActions, NoteContent, NoteMetadata } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { parser } from '@utils/parser';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function NoteKind_1({
|
||||
event,
|
||||
skipMetadata = false,
|
||||
}: {
|
||||
event: LumeEvent;
|
||||
event: NDKEvent;
|
||||
skipMetadata?: boolean;
|
||||
}) {
|
||||
const content = useMemo(() => parser(event), [event.id]);
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
|
||||
import { Image } from '@shared/image';
|
||||
import { NoteActions, NoteMetadata } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
function isImage(url: string) {
|
||||
return /\.(jpg|jpeg|gif|png|webp|avif)$/.test(url);
|
||||
}
|
||||
|
||||
export function NoteKind_1063({ event }: { event: LumeEvent }) {
|
||||
export function NoteKind_1063({ event }: { event: NDKEvent }) {
|
||||
const url = event.tags[0][1];
|
||||
|
||||
return (
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
|
||||
import {
|
||||
NoteActions,
|
||||
NoteContent,
|
||||
@ -8,25 +10,32 @@ import {
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useEvent } from '@utils/hooks/useEvent';
|
||||
import { getRepostID } from '@utils/transform';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function Repost({ event }: { event: LumeEvent }) {
|
||||
const repostID = getRepostID(event.tags);
|
||||
export function Repost({ event }: { event: NDKEvent }) {
|
||||
const repostID = event.tags.find((el) => el[0] === 'e')?.[1];
|
||||
const { status, data } = useEvent(repostID, event.content as unknown as string);
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 py-3">
|
||||
<NoteSkeleton />
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 py-3">
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<div className="flex items-center justify-center overflow-hidden rounded-xl bg-white/10 px-3 py-3">
|
||||
<p className="text-white/50">Failed to fetch event: {repostID}</p>
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="flex flex-col gap-1 overflow-hidden rounded-xl bg-white/10 px-3 py-3">
|
||||
<p className="select-text break-all text-white/50">
|
||||
Failed to get repostr with ID
|
||||
</p>
|
||||
<div className="break-all rounded-lg bg-white/10 px-2 py-2">
|
||||
<p className="text-white">{repostID}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { NoteActions, NoteContent, NoteMetadata, SubNote } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { parser } from '@utils/parser';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function NoteThread({
|
||||
event,
|
||||
root,
|
||||
reply,
|
||||
}: {
|
||||
event: LumeEvent;
|
||||
event: NDKEvent;
|
||||
root: string;
|
||||
reply: string;
|
||||
}) {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
|
||||
import { NoteActions, NoteMetadata } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function NoteKindUnsupport({ event }: { event: LumeEvent }) {
|
||||
export function NoteKindUnsupport({ event }: { event: NDKEvent }) {
|
||||
return (
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 pt-3">
|
||||
|
@ -2,6 +2,8 @@ import { memo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { MentionUser, NoteSkeleton } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
@ -11,13 +13,15 @@ import { useWidgets } from '@stores/widgets';
|
||||
import { useEvent } from '@utils/hooks/useEvent';
|
||||
|
||||
export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||
const { db } = useStorage();
|
||||
const { status, data } = useEvent(id);
|
||||
|
||||
const setWidget = useWidgets((state) => state.setWidget);
|
||||
|
||||
const openThread = (event, thread: string) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection.toString().length === 0) {
|
||||
setWidget({ kind: widgetKinds.thread, title: 'Thread', content: thread });
|
||||
setWidget(db, { kind: widgetKinds.thread, title: 'Thread', content: thread });
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
}
|
||||
@ -26,7 +30,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||
if (!id) {
|
||||
return (
|
||||
<div className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3">
|
||||
<p className="break-all">Failed to fetch event: {id}</p>
|
||||
<p className="break-all">Failed to get event with id: {id}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -64,14 +68,14 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{data?.content?.original?.length > 160
|
||||
? data.content.original.substring(0, 160) + '...'
|
||||
: data.content.original}
|
||||
{data?.content.length > 160
|
||||
? data.content.substring(0, 160) + '...'
|
||||
: data.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="break-all">Failed to fetch event: {id}</p>
|
||||
<p className="break-all">Failed to get event with id: {id}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,25 +1,29 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
|
||||
import { parser } from '@utils/parser';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function useEvent(id: string, embed?: string) {
|
||||
const { ndk } = useNDK();
|
||||
const { status, data, error, isFetching } = useQuery(
|
||||
['note', id],
|
||||
const { status, data, error } = useQuery(
|
||||
['event', id],
|
||||
async () => {
|
||||
if (embed) {
|
||||
const event: LumeEvent = JSON.parse(embed);
|
||||
if (event.kind === 1) embed['content'] = parser(event);
|
||||
return embed as unknown as LumeEvent;
|
||||
} else {
|
||||
const event = (await ndk.fetchEvent(id)) as LumeEvent;
|
||||
if (!event) throw new Error('event not found');
|
||||
if (event.kind === 1) event['content'] = parser(event) as unknown as string;
|
||||
return event as LumeEvent;
|
||||
const event: NDKEvent = JSON.parse(embed);
|
||||
// @ts-expect-error, #TODO: convert NDKEvent to ExNDKEvent
|
||||
if (event.kind === 1) event.content = parser(event);
|
||||
|
||||
return event as unknown as NDKEvent;
|
||||
}
|
||||
|
||||
const event = (await ndk.fetchEvent(id)) as NDKEvent;
|
||||
if (!event) throw new Error('event not found');
|
||||
// @ts-expect-error, #TODO: convert NDKEvent to ExNDKEvent
|
||||
if (event.kind === 1) event.content = parser(event);
|
||||
|
||||
return event as NDKEvent;
|
||||
},
|
||||
{
|
||||
staleTime: Infinity,
|
||||
@ -29,5 +33,5 @@ export function useEvent(id: string, embed?: string) {
|
||||
}
|
||||
);
|
||||
|
||||
return { status, data, error, isFetching };
|
||||
return { status, data, error };
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ import {
|
||||
NDKSubscription,
|
||||
NDKUser,
|
||||
} from '@nostr-dev-kit/ndk';
|
||||
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import { NostrFetcher } from 'nostr-fetch';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@ -16,17 +18,9 @@ import { useStorage } from '@libs/storage/provider';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
import { nHoursAgo } from '@utils/date';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
interface NotesResponse {
|
||||
status: string;
|
||||
data: LumeEvent[];
|
||||
nextCursor?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function useNostr() {
|
||||
const { ndk } = useNDK();
|
||||
const { ndk, relayUrls } = useNDK();
|
||||
const { db } = useStorage();
|
||||
|
||||
const privkey = useStronghold((state) => state.privkey);
|
||||
@ -81,30 +75,65 @@ export function useNostr() {
|
||||
await db.updateAccount('follows', [...follows]);
|
||||
await db.updateAccount('network', [...new Set([...follows, ...network])]);
|
||||
|
||||
return { status: 'ok' };
|
||||
// clear lru caches
|
||||
lruNetwork.clear();
|
||||
|
||||
return { status: 'ok', message: 'User data fetched' };
|
||||
} catch (e) {
|
||||
return { status: 'failed', message: e };
|
||||
}
|
||||
};
|
||||
|
||||
const fetchNotes = async (since: number): Promise<NotesResponse> => {
|
||||
const prefetchEvents = async () => {
|
||||
try {
|
||||
if (!ndk) return { status: 'failed', data: [], message: 'NDK instance not found' };
|
||||
|
||||
console.log('fetch all events since: ', since);
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
authors: db.account.network ?? db.account.follows,
|
||||
since: nHoursAgo(since),
|
||||
});
|
||||
// setup nostr-fetch
|
||||
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
|
||||
const dbEventsEmpty = await db.isEventsEmpty();
|
||||
|
||||
const sorted = [...events].sort(
|
||||
(a, b) => b.created_at - a.created_at
|
||||
) as unknown as LumeEvent[];
|
||||
let since: number;
|
||||
if (dbEventsEmpty) {
|
||||
since = nHoursAgo(24);
|
||||
} else {
|
||||
since = db.account.last_login_at ?? nHoursAgo(24);
|
||||
}
|
||||
|
||||
return { status: 'ok', data: sorted, nextCursor: since * 2 };
|
||||
console.log("prefetching events with user's network: ", db.account.network.length);
|
||||
console.log('prefetching events since: ', since);
|
||||
|
||||
const events = fetcher.allEventsIterator(
|
||||
relayUrls,
|
||||
{
|
||||
kinds: [1, 6],
|
||||
authors: db.account.network,
|
||||
},
|
||||
{ since: since }
|
||||
);
|
||||
|
||||
// save all events to database
|
||||
for await (const event of events) {
|
||||
let root: string;
|
||||
let reply: string;
|
||||
if (event.tags?.[0]?.[0] === 'e' && !event.tags?.[0]?.[3]) {
|
||||
root = event.tags[0][1];
|
||||
} else {
|
||||
root = event.tags.find((el) => el[3] === 'root')?.[1];
|
||||
reply = event.tags.find((el) => el[3] === 'reply')?.[1];
|
||||
}
|
||||
db.createEvent(
|
||||
event.id,
|
||||
JSON.stringify(event),
|
||||
event.pubkey,
|
||||
root,
|
||||
reply,
|
||||
event.created_at
|
||||
);
|
||||
}
|
||||
|
||||
return { status: 'ok', data: [], message: 'prefetch completed' };
|
||||
} catch (e) {
|
||||
console.error('failed get notes, error: ', e);
|
||||
console.error('prefetch events failed, error: ', e);
|
||||
return { status: 'failed', data: [], message: e };
|
||||
}
|
||||
};
|
||||
@ -150,5 +179,5 @@ export function useNostr() {
|
||||
return res;
|
||||
};
|
||||
|
||||
return { sub, fetchUserData, fetchNotes, publish, createZap };
|
||||
return { sub, fetchUserData, prefetchEvents, publish, createZap };
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import getUrls from 'get-urls';
|
||||
import { Event, parseReferences } from 'nostr-tools';
|
||||
import ReactPlayer from 'react-player';
|
||||
|
||||
import { LumeEvent, RichContent } from '@utils/types';
|
||||
import { RichContent } from '@utils/types';
|
||||
|
||||
export function parser(event: LumeEvent) {
|
||||
export function parser(event: NDKEvent) {
|
||||
if (event.kind !== 1) return;
|
||||
|
||||
const references = parseReferences(event as unknown as Event);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { NDKTag } from '@nostr-dev-kit/ndk';
|
||||
import { destr } from 'destr';
|
||||
|
||||
// convert array to NIP-02 tag list
|
||||
export function arrayToNIP02(arr: string[]) {
|
||||
@ -12,8 +11,7 @@ export function arrayToNIP02(arr: string[]) {
|
||||
}
|
||||
|
||||
// get repost id from event tags
|
||||
export function getRepostID(arr: NDKTag[]) {
|
||||
const tags = destr(arr) as string[];
|
||||
export function getRepostID(tags: NDKTag[]) {
|
||||
let quoteID = null;
|
||||
|
||||
if (tags.length > 0) {
|
||||
|
12
src/utils/types.d.ts
vendored
12
src/utils/types.d.ts
vendored
@ -8,8 +8,15 @@ export interface RichContent {
|
||||
links: string[];
|
||||
}
|
||||
|
||||
export interface LumeEvent extends NDKEvent {
|
||||
richContent: RichContent;
|
||||
export interface DBEvent {
|
||||
id: string;
|
||||
account_id: number;
|
||||
event: string | NDKEvent;
|
||||
author: string;
|
||||
root_id: string;
|
||||
reply_id: string;
|
||||
created_at: number;
|
||||
richContent?: RichContent;
|
||||
}
|
||||
|
||||
export interface Account extends NDKUserProfile {
|
||||
@ -20,6 +27,7 @@ export interface Account extends NDKUserProfile {
|
||||
network: null | string[];
|
||||
is_active: number;
|
||||
privkey?: string; // deprecated
|
||||
last_login_at: number;
|
||||
}
|
||||
|
||||
export interface Profile extends NDKUserProfile {
|
||||
|
Loading…
x
Reference in New Issue
Block a user