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