clean up and improve perf

This commit is contained in:
Ren Amamiya
2023-09-28 16:18:04 +07:00
parent 4f4e2f5ccd
commit cb3c95b133
19 changed files with 246 additions and 137 deletions

View File

@@ -21,8 +21,10 @@
"@dnd-kit/core": "^6.0.8", "@dnd-kit/core": "^6.0.8",
"@getalby/sdk": "^2.4.0", "@getalby/sdk": "^2.4.0",
"@nostr-dev-kit/ndk": "^1.3.0", "@nostr-dev-kit/ndk": "^1.3.0",
"@nostr-dev-kit/ndk-cache-dexie": "^1.3.0",
"@nostr-fetch/adapter-ndk": "^0.12.2", "@nostr-fetch/adapter-ndk": "^0.12.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",

48
pnpm-lock.yaml generated
View File

@@ -14,12 +14,18 @@ dependencies:
'@nostr-dev-kit/ndk': '@nostr-dev-kit/ndk':
specifier: ^1.3.0 specifier: ^1.3.0
version: 1.3.0(typescript@5.2.2) version: 1.3.0(typescript@5.2.2)
'@nostr-dev-kit/ndk-cache-dexie':
specifier: ^1.3.0
version: 1.3.0(typescript@5.2.2)
'@nostr-fetch/adapter-ndk': '@nostr-fetch/adapter-ndk':
specifier: ^0.12.2 specifier: ^0.12.2
version: 0.12.2(@nostr-dev-kit/ndk@1.3.0)(nostr-fetch@0.13.0) version: 0.12.2(@nostr-dev-kit/ndk@1.3.0)(nostr-fetch@0.13.0)
'@radix-ui/react-alert-dialog': '@radix-ui/react-alert-dialog':
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0) version: 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-avatar':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-collapsible': '@radix-ui/react-collapsible':
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0) version: 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0)
@@ -782,6 +788,19 @@ packages:
fastq: 1.15.0 fastq: 1.15.0
dev: true dev: true
/@nostr-dev-kit/ndk-cache-dexie@1.3.0(typescript@5.2.2):
resolution: {integrity: sha512-T9c2d/CennLXm/VO+4oKQgJln5+4GATM/Ko+QDkjOOKryqCYldap2QkIZ27IAsfhgGqMueyiU5g3oOcZFBgejQ==}
dependencies:
'@nostr-dev-kit/ndk': 1.3.0(typescript@5.2.2)
debug: 4.3.4
dexie: 3.2.4
nostr-tools: 1.16.0(typescript@5.2.2)
typescript-lru-cache: 2.0.0
transitivePeerDependencies:
- supports-color
- typescript
dev: false
/@nostr-dev-kit/ndk@1.3.0(typescript@5.2.2): /@nostr-dev-kit/ndk@1.3.0(typescript@5.2.2):
resolution: {integrity: sha512-ZRne/TF1IRqy35ZwLq3452YD+AUux0r5A+mZgHuGNRv7GJzr2gUl1QNRjs+Db+emXdaHn82LKTRn6cFrni1oPA==} resolution: {integrity: sha512-ZRne/TF1IRqy35ZwLq3452YD+AUux0r5A+mZgHuGNRv7GJzr2gUl1QNRjs+Db+emXdaHn82LKTRn6cFrni1oPA==}
dependencies: dependencies:
@@ -883,6 +902,30 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
/@radix-ui/react-avatar@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.1
'@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0)
'@types/react': 18.2.23
'@types/react-dom': 18.2.8
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0): /@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
peerDependencies: peerDependencies:
@@ -3225,6 +3268,11 @@ packages:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dev: false dev: false
/dexie@3.2.4:
resolution: {integrity: sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA==}
engines: {node: '>=6.0'}
dev: false
/didyoumean@1.2.2: /didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dev: true dev: true

View File

@@ -1,9 +0,0 @@
-- Add migration script here
CREATE TABLE
metadata (
id TEXT NOT NULL PRIMARY KEY,
event TEXT NOT NULL,
author TEXT NOT NULL,
kind NUMBER NOT NULL DEFAULt 0,
created_at INTEGER NOT NULL
);

View File

@@ -239,12 +239,6 @@ fn main() {
sql: include_str!("../migrations/20230918235335_add_uniq_to_relay.sql"), sql: include_str!("../migrations/20230918235335_add_uniq_to_relay.sql"),
kind: MigrationKind::Up, kind: MigrationKind::Up,
}, },
Migration {
version: 20230921085234,
description: "add metadata",
sql: include_str!("../migrations/20230921085234_add_metadata_table.sql"),
kind: MigrationKind::Up,
},
], ],
) )
.build(), .build(),

View File

@@ -1,20 +1,18 @@
import NDK from '@nostr-dev-kit/ndk'; import NDK from '@nostr-dev-kit/ndk';
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie';
import { ndkAdapter } from '@nostr-fetch/adapter-ndk'; import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
import { message } from '@tauri-apps/api/dialog'; import { message } from '@tauri-apps/api/dialog';
import { fetch } from '@tauri-apps/api/http'; import { fetch } from '@tauri-apps/api/http';
import { NostrFetcher } from 'nostr-fetch'; import { NostrFetcher } from 'nostr-fetch';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import TauriAdapter from '@libs/ndk/cache';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
export const NDKInstance = () => { export const NDKInstance = () => {
const { db } = useStorage();
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(), []); const { db } = useStorage();
const fetcher = useMemo( const fetcher = useMemo(
() => (ndk ? NostrFetcher.withCustomPool(ndkAdapter(ndk)) : null), () => (ndk ? NostrFetcher.withCustomPool(ndkAdapter(ndk)) : null),
[ndk] [ndk]
@@ -57,9 +55,10 @@ export const NDKInstance = () => {
async function initNDK() { async function initNDK() {
const explicitRelayUrls = await getExplicitRelays(); const explicitRelayUrls = await getExplicitRelays();
const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'lume_ndkcache' });
const instance = new NDK({ const instance = new NDK({
explicitRelayUrls, explicitRelayUrls,
cacheAdapter, cacheAdapter: dexieAdapter,
}); });
try { try {
@@ -77,10 +76,6 @@ export const NDKInstance = () => {
useEffect(() => { useEffect(() => {
if (!ndk) initNDK(); if (!ndk) initNDK();
return () => {
cacheAdapter.saveCache();
};
}, []); }, []);
return { return {

View File

@@ -1,4 +1,4 @@
import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { BaseDirectory, removeFile } from '@tauri-apps/api/fs'; import { BaseDirectory, removeFile } from '@tauri-apps/api/fs';
import { Platform } from '@tauri-apps/api/os'; import { Platform } from '@tauri-apps/api/os';
import Database from 'tauri-plugin-sql-api'; import Database from 'tauri-plugin-sql-api';
@@ -6,7 +6,6 @@ import { Stronghold } from 'tauri-plugin-stronghold-api';
import { FULL_RELAYS } from '@stores/constants'; import { FULL_RELAYS } from '@stores/constants';
import { toRawEvent } from '@utils/rawEvent';
import { Account, DBEvent, Relays, Widget } from '@utils/types'; import { Account, DBEvent, Relays, Widget } from '@utils/types';
export class LumeStorage { export class LumeStorage {
@@ -124,6 +123,7 @@ export class LumeStorage {
public async updateLastLogin() { public async updateLastLogin() {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
this.account.last_login_at = now;
return await this.db.execute( return await this.db.execute(
'UPDATE accounts SET last_login_at = $1 WHERE id = $2;', 'UPDATE accounts SET last_login_at = $1 WHERE id = $2;',
[now, this.account.id] [now, this.account.id]
@@ -298,38 +298,6 @@ export class LumeStorage {
return results.length < 1; return results.length < 1;
} }
public async createMetadata(event: NDKEvent) {
const rawEvent = toRawEvent(event);
return await this.db.execute(
'INSERT OR IGNORE INTO metadata (id, event, author, kind, created_at) VALUES ($1, $2, $3, $4, $5);',
[
rawEvent.id,
JSON.stringify(rawEvent),
rawEvent.pubkey,
rawEvent.kind,
rawEvent.created_at,
]
);
}
public async createProfile(pubkey: string, profile: NDKUserProfile) {
return await this.db.execute(
'INSERT OR REPLACE INTO metadata (id, event, author, kind, created_at) VALUES ($1, $2, $3, $4, $5);',
[pubkey, JSON.stringify(profile), pubkey, 0, Math.round(Date.now() / 1000)]
);
}
public async getMetadataByPubkey(pubkey: string) {
const results: DBEvent[] = await this.db.select(
'SELECT * FROM metadata WHERE author = $1 AND kind = "0" LIMIT 1;',
[pubkey]
);
if (results.length < 1) return null;
return JSON.parse(results[0].event as string) as NDKEvent;
}
public async getExplicitRelayUrls() { public async getExplicitRelayUrls() {
if (!this.account) return FULL_RELAYS; if (!this.account) return FULL_RELAYS;

View File

@@ -34,7 +34,7 @@ export function ComposerModal() {
<ComposeIcon className="h-4 w-4 text-white" /> <ComposeIcon className="h-4 w-4 text-white" />
</button> </button>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal className="relative z-10"> <Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" /> <Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center"> <Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div <div

View File

@@ -1,7 +1,10 @@
import { minidenticon } from 'minidenticons'; import { minidenticon } from 'minidenticons';
import { ImgHTMLAttributes, useState } from 'react'; import { ImgHTMLAttributes, memo, useState } from 'react';
export function Image({ src, ...props }: ImgHTMLAttributes<HTMLImageElement>) { export const Image = memo(function Image({
src,
...props
}: ImgHTMLAttributes<HTMLImageElement>) {
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
if (isError || !src) { if (isError || !src) {
@@ -20,9 +23,10 @@ export function Image({ src, ...props }: ImgHTMLAttributes<HTMLImageElement>) {
currentTarget.onerror = null; currentTarget.onerror = null;
setIsError(true); setIsError(true);
}} }}
loading="lazy"
decoding="async" decoding="async"
alt="lume default img" alt="lume default img"
style={{ contentVisibility: 'auto' }} style={{ contentVisibility: 'auto' }}
/> />
); );
} });

View File

@@ -1,13 +1,23 @@
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import { NavLink } from 'react-router-dom'; import { NavLink, useNavigate } from 'react-router-dom';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { ChatsList } from '@app/chats/components/list'; import { ChatsList } from '@app/chats/components/list';
import { useStorage } from '@libs/storage/provider';
import { ActiveAccount } from '@shared/accounts/active'; import { ActiveAccount } from '@shared/accounts/active';
import { ComposerModal } from '@shared/composer'; import { ComposerModal } from '@shared/composer';
import { Frame } from '@shared/frame'; import { Frame } from '@shared/frame';
import { BellIcon, NavArrowDownIcon, NwcIcon, SpaceIcon, WorldIcon } from '@shared/icons'; import {
ArrowLeftIcon,
ArrowRightIcon,
BellIcon,
NavArrowDownIcon,
NwcIcon,
SpaceIcon,
WorldIcon,
} from '@shared/icons';
import { useActivities } from '@stores/activities'; import { useActivities } from '@stores/activities';
import { useSidebar } from '@stores/sidebar'; import { useSidebar } from '@stores/sidebar';
@@ -15,6 +25,9 @@ import { useSidebar } from '@stores/sidebar';
import { compactNumber } from '@utils/number'; import { compactNumber } from '@utils/number';
export function Navigation() { export function Navigation() {
const { db } = useStorage();
const navigate = useNavigate();
const totalNewActivities = useActivities((state) => state.totalNewActivities); const totalNewActivities = useActivities((state) => state.totalNewActivities);
const [chats, toggleChats] = useSidebar((state) => [state.chats, state.toggleChats]); const [chats, toggleChats] = useSidebar((state) => [state.chats, state.toggleChats]);
@@ -30,8 +43,28 @@ export function Navigation() {
> >
<div <div
data-tauri-drag-region data-tauri-drag-region
className="inline-flex h-16 w-full items-center justify-end px-3" className="inline-flex h-16 w-full items-center justify-between px-3"
> >
{db.platform !== 'darwin' ? (
<div className="inline-flex items-center gap-4 pl-2">
<button
type="button"
onClick={() => navigate(-1)}
className="inline-flex h-9 items-center justify-center"
>
<ArrowLeftIcon className="h-5 w-5 text-white/50" />
</button>
<button
type="button"
onClick={() => navigate(1)}
className="inline-flex h-9 items-center justify-center"
>
<ArrowRightIcon className="h-5 w-5 text-white/50" />
</button>
</div>
) : (
<div />
)}
<ComposerModal /> <ComposerModal />
</div> </div>
<div <div

View File

@@ -49,7 +49,7 @@ export function ChildNote({ id, root }: { id: string; root?: string }) {
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" /> <div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" />
<div className="relative mb-5 flex flex-col"> <div className="relative mb-5 flex flex-col">
<div className="relative z-10 flex items-start gap-3"> <div className="relative z-10 flex items-start gap-3">
<div className="inline-flex h-11 w-11 items-end justify-center rounded-lg bg-black pb-1"> <div className="inline-flex h-10 w-10 items-end justify-center rounded-lg bg-black pb-1">
<img src="/lume.png" alt="lume" className="h-auto w-1/3" /> <img src="/lume.png" alt="lume" className="h-auto w-1/3" />
</div> </div>
<h5 className="truncate font-semibold leading-none text-white"> <h5 className="truncate font-semibold leading-none text-white">

View File

@@ -12,14 +12,20 @@ export function Reply({ event, root }: { event: NDKEventWithReplies; root?: stri
<div className="relative z-10"> <div className="relative z-10">
<div className="relative flex flex-col"> <div className="relative flex flex-col">
<User pubkey={event.pubkey} time={event.created_at} /> <User pubkey={event.pubkey} time={event.created_at} />
<div className="-mt-6 flex items-start gap-3"> <div className="-mt-5 flex items-start gap-3">
<div className="w-11 shrink-0" /> <div className="w-10 shrink-0" />
<div className="flex-1"> <div className="flex-1">
<TextNote content={event.content} /> <TextNote content={event.content} />
<NoteActions id={event.id} pubkey={event.pubkey} root={root} /> <NoteActions
id={event.id}
pubkey={event.pubkey}
root={root}
extraButtons={false}
/>
</div> </div>
</div> </div>
</div> </div>
<div className="pl-14">
{event.replies ? ( {event.replies ? (
event.replies.map((sub) => <SubReply key={sub.id} event={sub} />) event.replies.map((sub) => <SubReply key={sub.id} event={sub} />)
) : ( ) : (
@@ -27,5 +33,6 @@ export function Reply({ event, root }: { event: NDKEventWithReplies; root?: stri
)} )}
</div> </div>
</div> </div>
</div>
); );
} }

View File

@@ -51,7 +51,7 @@ export function RepliesList({ id }: { id: string }) {
<h5 className="mb-2 text-lg font-semibold text-white"> <h5 className="mb-2 text-lg font-semibold text-white">
{data?.length || 0} replies {data?.length || 0} replies
</h5> </h5>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-5">
{data?.length === 0 ? ( {data?.length === 0 ? (
<div className="mt-2 flex w-full items-center justify-center rounded-xl bg-white/10 backdrop-blur-xl"> <div className="mt-2 flex w-full items-center justify-center rounded-xl bg-white/10 backdrop-blur-xl">
<div className="flex flex-col items-center justify-center gap-2 py-6"> <div className="flex flex-col items-center justify-center gap-2 py-6">

View File

@@ -7,11 +7,11 @@ export function SubReply({ event }: { event: NDKEvent }) {
return ( return (
<div className="relative z-10 mb-3 mt-5 flex flex-col"> <div className="relative z-10 mb-3 mt-5 flex flex-col">
<User pubkey={event.pubkey} time={event.created_at} /> <User pubkey={event.pubkey} time={event.created_at} />
<div className="-mt-6 flex items-start gap-3"> <div className="-mt-5 flex items-start gap-3">
<div className="w-11 shrink-0" /> <div className="w-10 shrink-0" />
<div className="flex-1"> <div className="flex-1">
<TextNote /> <TextNote content={event.content} />
<NoteActions id={event.id} pubkey={event.pubkey} /> <NoteActions id={event.id} pubkey={event.pubkey} extraButtons={false} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,8 @@ export function NoteSkeleton() {
<div className="relative h-11 w-11 shrink overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl" /> <div className="relative h-11 w-11 shrink overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl" />
<div className="h-3 w-20 rounded bg-white/10 backdrop-blur-xl" /> <div className="h-3 w-20 rounded bg-white/10 backdrop-blur-xl" />
</div> </div>
<div className="-mt-6 animate-pulse pl-[49px]"> <div className="-mt-5 flex animate-pulse gap-3">
<div className="w-10 shrink-0" />
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="h-3 w-full rounded bg-white/10 backdrop-blur-xl" /> <div className="h-3 w-full rounded bg-white/10 backdrop-blur-xl" />
<div className="h-3 w-2/3 rounded bg-white/10 backdrop-blur-xl" /> <div className="h-3 w-2/3 rounded bg-white/10 backdrop-blur-xl" />

View File

@@ -1,11 +1,12 @@
import * as Avatar from '@radix-ui/react-avatar';
import * as HoverCard from '@radix-ui/react-hover-card'; import * as HoverCard from '@radix-ui/react-hover-card';
import { minidenticon } from 'minidenticons';
import { memo } from 'react'; import { memo } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import { RepostIcon, WorldIcon } from '@shared/icons'; import { RepostIcon, WorldIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { NIP05 } from '@shared/nip05'; import { NIP05 } from '@shared/nip05';
import { formatCreatedAt } from '@utils/createdAt'; import { formatCreatedAt } from '@utils/createdAt';
@@ -32,7 +33,10 @@ export const User = memo(function User({
embedProfile?: string; embedProfile?: string;
}) { }) {
const { status, user } = useProfile(pubkey, embedProfile); const { status, user } = useProfile(pubkey, embedProfile);
const createdAt = time ? formatCreatedAt(time, variant === 'chat') : 0;
const createdAt = formatCreatedAt(time, variant === 'chat');
const svgURI =
'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50));
if (status === 'loading') { if (status === 'loading') {
if (variant === 'avatar') { if (variant === 'avatar') {
@@ -51,8 +55,8 @@ export const User = memo(function User({
} }
return ( return (
<div className="relative flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="relative z-10 h-10 w-10 shrink-0 animate-pulse overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl" /> <div className="h-10 w-10 shrink-0 animate-pulse overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl" />
<div className="h-3.5 w-36 animate-pulse rounded bg-white/10 backdrop-blur-xl" /> <div className="h-3.5 w-36 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
</div> </div>
); );
@@ -60,14 +64,20 @@ export const User = memo(function User({
if (variant === 'mention') { if (variant === 'mention') {
return ( return (
<div className="relative z-10 flex items-center gap-2"> <div className="flex items-center gap-2">
<button type="button" className="relative z-40 h-6 w-6 shrink-0 overflow-hidden"> <Avatar.Root className="shrink-0">
<Image <Avatar.Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={pubkey} alt={pubkey}
className="h-6 w-6 rounded object-cover" loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-6 w-6 rounded"
/> />
</button> <Avatar.Fallback delayMs={300}>
<img src={svgURI} alt={pubkey} className="h-6 w-6 rounded bg-black" />
</Avatar.Fallback>
</Avatar.Root>
<div className="flex flex-1 items-baseline gap-2"> <div className="flex flex-1 items-baseline gap-2">
<h5 className="max-w-[10rem] truncate font-semibold leading-none text-white"> <h5 className="max-w-[10rem] truncate font-semibold leading-none text-white">
{user?.name || {user?.name ||
@@ -85,11 +95,19 @@ export const User = memo(function User({
if (variant === 'large') { if (variant === 'large') {
return ( return (
<div className="flex h-full w-full flex-col gap-2.5"> <div className="flex h-full w-full flex-col gap-2.5">
<Image <Avatar.Root className="shrink-0">
<Avatar.Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={pubkey} alt={pubkey}
className="h-14 w-14 shrink-0 rounded-lg object-cover" loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-14 w-14 rounded-lg"
/> />
<Avatar.Fallback delayMs={300}>
<img src={svgURI} alt={pubkey} className="h-14 w-14 rounded-lg bg-black" />
</Avatar.Fallback>
</Avatar.Root>
<div className="flex h-full flex-col items-start justify-between"> <div className="flex h-full flex-col items-start justify-between">
<div className="flex flex-col items-start gap-1 text-start"> <div className="flex flex-col items-start gap-1 text-start">
<p className="max-w-[15rem] truncate text-lg font-semibold leading-none text-white"> <p className="max-w-[15rem] truncate text-lg font-semibold leading-none text-white">
@@ -125,11 +143,19 @@ export const User = memo(function User({
if (variant === 'simple') { if (variant === 'simple') {
return ( return (
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<Image <Avatar.Root className="shrink-0">
<Avatar.Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={pubkey} alt={pubkey}
className="h-12 w-12 shrink-0 rounded-lg object-cover" loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-12 w-12 rounded-lg"
/> />
<Avatar.Fallback delayMs={300}>
<img src={svgURI} alt={pubkey} className="h-12 w-12 rounded-lg bg-black" />
</Avatar.Fallback>
</Avatar.Root>
<div className="flex w-full flex-col items-start gap-1"> <div className="flex w-full flex-col items-start gap-1">
<h3 className="max-w-[15rem] truncate font-medium leading-none text-white"> <h3 className="max-w-[15rem] truncate font-medium leading-none text-white">
{user?.name || user?.display_name || user?.displayName} {user?.name || user?.display_name || user?.displayName}
@@ -144,11 +170,19 @@ export const User = memo(function User({
if (variant === 'avatar') { if (variant === 'avatar') {
return ( return (
<Image <Avatar.Root>
<Avatar.Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={pubkey} alt={pubkey}
className="h-12 w-12 shrink-0 rounded-lg object-cover" loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-12 w-12 rounded-lg"
/> />
<Avatar.Fallback delayMs={300}>
<img src={svgURI} alt={pubkey} className="h-12 w-12 rounded-lg bg-black" />
</Avatar.Fallback>
</Avatar.Root>
); );
} }
@@ -159,11 +193,19 @@ export const User = memo(function User({
<RepostIcon className="h-6 w-6 text-blue-500" /> <RepostIcon className="h-6 w-6 text-blue-500" />
</div> </div>
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<Image <Avatar.Root className="shrink-0">
<Avatar.Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={pubkey} alt={pubkey}
className="relative z-20 inline-block h-6 w-6 rounded" loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-6 w-6 rounded"
/> />
<Avatar.Fallback delayMs={300}>
<img src={svgURI} alt={pubkey} className="h-6 w-6 rounded bg-black" />
</Avatar.Fallback>
</Avatar.Root>
<div className="inline-flex items-baseline gap-1"> <div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[10rem] truncate font-medium leading-none text-white/80"> <h5 className="max-w-[10rem] truncate font-medium leading-none text-white/80">
{user?.name || {user?.name ||
@@ -181,14 +223,22 @@ export const User = memo(function User({
if (variant === 'thread') { if (variant === 'thread') {
return ( return (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Image <Avatar.Root className="shrink-0">
<Avatar.Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={pubkey} alt={pubkey}
className="relative z-20 inline-block h-10 w-10 rounded-lg" loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-10 w-10 rounded-lg"
/> />
<Avatar.Fallback delayMs={300}>
<img src={svgURI} alt={pubkey} className="h-10 w-10 rounded-lg bg-black" />
</Avatar.Fallback>
</Avatar.Root>
<div className="flex flex-1 flex-col gap-2"> <div className="flex flex-1 flex-col gap-2">
<h5 className="max-w-[15rem] truncate font-semibold leading-none text-white"> <h5 className="max-w-[15rem] truncate font-semibold leading-none text-white">
{user?.name || user?.display_name || user?.displayName} {user?.name || user?.display_name || user?.displayName || 'Anon'}
</h5> </h5>
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<span className="leading-none text-white/50">{createdAt}</span> <span className="leading-none text-white/50">{createdAt}</span>
@@ -204,16 +254,23 @@ export const User = memo(function User({
<HoverCard.Root> <HoverCard.Root>
<div className="relative z-10 flex items-start gap-3"> <div className="relative z-10 flex items-start gap-3">
<HoverCard.Trigger asChild> <HoverCard.Trigger asChild>
<button <Avatar.Root className="shrink-0">
type="button" <Avatar.Image
className="relative z-40 h-10 w-10 shrink-0 overflow-hidden"
>
<Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={pubkey} alt={pubkey}
className="h-10 w-10 rounded-lg object-cover" loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-10 w-10 rounded-lg border border-white/5"
/> />
</button> <Avatar.Fallback delayMs={300}>
<img
src={svgURI}
alt={pubkey}
className="h-10 w-10 rounded-lg border border-white/5 bg-black"
/>
</Avatar.Fallback>
</Avatar.Root>
</HoverCard.Trigger> </HoverCard.Trigger>
<div className="flex flex-1 items-baseline gap-2"> <div className="flex flex-1 items-baseline gap-2">
<h5 className="max-w-[15rem] truncate font-semibold leading-none text-white"> <h5 className="max-w-[15rem] truncate font-semibold leading-none text-white">
@@ -232,11 +289,23 @@ export const User = memo(function User({
sideOffset={5} sideOffset={5}
> >
<div className="flex gap-2.5 border-b border-white/5 px-3 py-3"> <div className="flex gap-2.5 border-b border-white/5 px-3 py-3">
<Image <Avatar.Root className="shrink-0">
<Avatar.Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={pubkey} alt={pubkey}
className="h-10 w-10 shrink-0 rounded-lg object-cover" loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-10 w-10 rounded-lg border border-white/5"
/> />
<Avatar.Fallback delayMs={300}>
<img
src={svgURI}
alt={pubkey}
className="h-10 w-10 rounded-lg border border-white/5 bg-black"
/>
</Avatar.Fallback>
</Avatar.Root>
<div className="flex flex-1 flex-col gap-2"> <div className="flex flex-1 flex-col gap-2">
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<h5 className="text-sm font-semibold leading-none"> <h5 className="text-sm font-semibold leading-none">

View File

@@ -19,6 +19,8 @@ export function EventLoader({ firstTime }: { firstTime: boolean }) {
useEffect(() => { useEffect(() => {
async function getEvents() { async function getEvents() {
const events = await getAllEventsSinceLastLogin(); const events = await getAllEventsSinceLastLogin();
console.log('total event found: ', events.data.length);
const promises = await Promise.all( const promises = await Promise.all(
events.data.map(async (event) => await db.createEvent(event)) events.data.map(async (event) => await db.createEvent(event))
); );

View File

@@ -265,7 +265,7 @@ export function useNostr() {
if (!customSince) { if (!customSince) {
if (dbEventsEmpty || db.account.last_login_at === 0) { if (dbEventsEmpty || db.account.last_login_at === 0) {
since = db.account.network.length > 400 ? nHoursAgo(12) : nHoursAgo(24); since = db.account.network.length > 500 ? nHoursAgo(12) : nHoursAgo(24);
} else { } else {
since = db.account.last_login_at; since = db.account.last_login_at;
} }

View File

@@ -2,10 +2,8 @@ import { NDKUserProfile } 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 { useStorage } from '@libs/storage/provider';
export function useProfile(pubkey: string, embed?: string) { export function useProfile(pubkey: string, embed?: string) {
const { db } = useStorage();
const { ndk } = useNDK(); const { ndk } = useNDK();
const { const {
status, status,
@@ -21,13 +19,7 @@ export function useProfile(pubkey: string, embed?: string) {
const cleanPubkey = pubkey.replace('-', ''); const cleanPubkey = pubkey.replace('-', '');
const user = ndk.getUser({ hexpubkey: cleanPubkey }); const user = ndk.getUser({ hexpubkey: cleanPubkey });
return await user.fetchProfile();
const profile = await user.fetchProfile({ closeOnEose: true });
if (!user.profile) return Promise.reject(new Error('profile not found'));
await db.createProfile(cleanPubkey, profile);
return user.profile;
}, },
{ {
enabled: !!ndk, enabled: !!ndk,
@@ -35,6 +27,7 @@ export function useProfile(pubkey: string, embed?: string) {
refetchOnMount: false, refetchOnMount: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnReconnect: false, refetchOnReconnect: false,
retry: 2,
} }
); );

View File

@@ -20,6 +20,8 @@ function isURL(string: string) {
} }
export function parser(eventContent: string) { export function parser(eventContent: string) {
if (!eventContent) return '';
try { try {
const content: RichContent = { const content: RichContent = {
parsed: null, parsed: null,