improve notification and performance

This commit is contained in:
2023-11-30 16:02:28 +07:00
parent 6f68c2762b
commit 80f675cb54
11 changed files with 356 additions and 291 deletions

View File

@@ -35,18 +35,18 @@ export function HomeScreen() {
queryFn: async () => {
const dbWidgets = await db.getWidgets();
const defaultWidgets = [
{
id: '9998',
title: 'Notification',
content: '',
kind: WIDGET_KIND.notification,
},
{
id: '9999',
title: 'Newsfeed',
content: '',
kind: WIDGET_KIND.newsfeed,
},
{
id: '9998',
title: 'Notification',
content: '',
kind: WIDGET_KIND.notification,
},
];
return [...defaultWidgets, ...dbWidgets];

View File

@@ -66,7 +66,7 @@ export const NDKInstance = () => {
// connect
await instance.connect();
const tmpFetcher = NostrFetcher.withCustomPool(ndkAdapter(instance));
const _fetcher = NostrFetcher.withCustomPool(ndkAdapter(instance));
// update account's metadata
if (db.account) {
@@ -88,7 +88,7 @@ export const NDKInstance = () => {
const rootIds = new Set();
const dedupQueue = new Set();
const events = await tmpFetcher.fetchLatestEvents(
const events = await _fetcher.fetchLatestEvents(
explicitRelayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost],
@@ -127,7 +127,7 @@ export const NDKInstance = () => {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await tmpFetcher.fetchLatestEvents(
const events = await _fetcher.fetchLatestEvents(
explicitRelayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
@@ -147,7 +147,7 @@ export const NDKInstance = () => {
}
setNDK(instance);
setFetcher(tmpFetcher);
setFetcher(_fetcher);
setRelayUrls(explicitRelayUrls);
} catch (e) {
const yes = await ask(

View File

@@ -73,7 +73,7 @@ export class LumeStorage {
[pubkey]
);
if (results.length < 1) return null;
if (!results.length) return null;
if (typeof results[0].profile === 'string')
results[0].profile = JSON.parse(results[0].profile);
@@ -87,7 +87,7 @@ export class LumeStorage {
[id]
);
if (results.length < 1) return null;
if (!results.length) return null;
return results[0];
}
@@ -98,7 +98,7 @@ export class LumeStorage {
`SELECT * FROM ndk_events WHERE id IN (${idsArr}) ORDER BY id;`
);
if (results.length < 1) return [];
if (!results.length) return [];
return results;
}
@@ -108,7 +108,7 @@ export class LumeStorage {
[pubkey]
);
if (results.length < 1) return [];
if (!results.length) return [];
return results;
}
@@ -118,7 +118,7 @@ export class LumeStorage {
[kind]
);
if (results.length < 1) return [];
if (!results.length) return [];
return results;
}
@@ -128,7 +128,7 @@ export class LumeStorage {
[kind, pubkey]
);
if (results.length < 1) return [];
if (!results.length) return [];
return results;
}
@@ -138,7 +138,7 @@ export class LumeStorage {
[tagValue]
);
if (results.length < 1) return [];
if (!results.length) return [];
return results;
}

View File

@@ -12,6 +12,9 @@ const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
queries: {
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // 10 seconds
},
},
},
});
@@ -22,7 +25,7 @@ const root = createRoot(container);
root.render(
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<Toaster position="top-center" closeButton theme="system" />
<Toaster position="top-center" theme="system" closeButton />
<StorageProvider>
<NDKProvider>
<App />

View File

@@ -7,14 +7,14 @@ export function TextKind({ content, textmode }: { content: string; textmode?: bo
if (textmode) {
return (
<div className="break-p line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
<div className="line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
{parsedContent}
</div>
);
}
return (
<div className={'min-w-0 px-3'}>
<div className="min-w-0 px-3">
<div className="break-p select-text leading-normal text-neutral-900 dark:text-neutral-100">
{parsedContent}
</div>

View File

@@ -1,109 +1,158 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { memo } from 'react';
import { ShareIcon } from '@shared/icons';
import {
MemoizedArticleKind,
MemoizedFileKind,
MemoizedTextKind,
NoteSkeleton,
} from '@shared/notes';
import { ReplyIcon, RepostIcon } from '@shared/icons';
import { ChildNote, TextKind } from '@shared/notes';
import { User } from '@shared/user';
import { WIDGET_KIND } from '@stores/constants';
import { formatCreatedAt } from '@utils/createdAt';
import { useEvent } from '@utils/hooks/useEvent';
import { useNostr } from '@utils/hooks/useNostr';
import { useWidget } from '@utils/hooks/useWidget';
export function NotifyNote({ event }: { event: NDKEvent }) {
const createdAt = formatCreatedAt(event.created_at, false);
const rootEventId = event.tags.find((el) => el[0] === 'e')?.[1];
const { status, data } = useEvent(rootEventId);
const { getEventThread } = useNostr();
const { addWidget } = useWidget();
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <MemoizedTextKind key={event.id} content={event.content} textmode />;
case NDKKind.Article:
return <MemoizedArticleKind key={event.id} id={event.id} tags={event.tags} />;
case 1063:
return <MemoizedFileKind key={event.id} tags={event.tags} />;
default:
return (
<div className="break-p line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
{event.content}
</div>
);
}
};
const thread = getEventThread(event.tags);
const createdAt = formatCreatedAt(event.created_at, false);
const renderText = (kind: number) => {
switch (kind) {
case NDKKind.Text:
return 'replied';
case NDKKind.Reaction: {
return `reacted your post`;
}
case NDKKind.Repost:
return 'reposted your post';
case NDKKind.Zap:
return 'zapped your post';
default:
return 'unknown';
}
};
if (status === 'pending') {
if (event.kind === NDKKind.Reaction) {
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
<div className="mb-3 h-min w-full px-3">
<div className="flex flex-col gap-2 rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
<div className="flex h-10 items-center justify-between">
<div className="relative flex w-full items-center gap-2 px-3 pt-2">
<div className="absolute -left-0.5 -top-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-blue-100 text-xs ring-2 ring-neutral-50 dark:bg-blue-900 dark:ring-neutral-950">
{event.content === '+' ? '👍' : event.content}
</div>
<div className="flex flex-1 items-center justify-between">
<div className="inline-flex items-center gap-1.5">
<User pubkey={event.pubkey} variant="notify" />
<p className="text-neutral-700 dark:text-neutral-300">reacted</p>
</div>
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="w-full px-3">
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
{thread.rootEventId ? <ChildNote id={thread.rootEventId} /> : null}
</div>
</div>
<button
type="button"
onClick={() =>
addWidget.mutate({
kind: WIDGET_KIND.thread,
title: 'Thread',
content: thread.rootEventId,
})
}
className="self-start text-blue-500 hover:text-blue-600"
>
Show original post
</button>
</div>
</div>
</div>
);
}
return (
<div className="mb-3 h-min w-full px-3">
<div className="flex flex-col gap-2 rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
<div className="flex h-10 items-center justify-between">
<div className="relative flex w-full items-center gap-2 px-3 pt-2">
<div className="absolute -left-0.5 -top-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-blue-100 text-xs ring-2 ring-neutral-50 dark:bg-blue-900 dark:ring-neutral-950">
{event.kind === 7 ? (event.content === '+' ? '👍' : event.content) : '⚡️'}
</div>
<div className="flex flex-1 items-center justify-between">
<div className="inline-flex items-center gap-1.5">
<User pubkey={event.pubkey} variant="notify" />
<p className="text-neutral-900 dark:text-neutral-100">
{renderText(event.kind)}
</p>
if (event.kind === NDKKind.Repost) {
return (
<div className="mb-3 h-min w-full px-3">
<div className="flex flex-col gap-2 rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
<div className="flex h-10 items-center justify-between">
<div className="relative flex w-full items-center gap-2 px-3 pt-2">
<div className="absolute -left-0.5 -top-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-teal-500 text-xs ring-2 ring-neutral-50 dark:ring-neutral-950">
<RepostIcon className="h-4 w-4 text-white" />
</div>
<div className="flex flex-1 items-center justify-between">
<div className="inline-flex items-center gap-1.5">
<User pubkey={event.pubkey} variant="notify" />
<p className="text-neutral-700 dark:text-neutral-300">reposted</p>
</div>
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
</div>
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
</div>
</div>
</div>
<div className="flex gap-2">
<div className="flex-1">{data ? renderKind(data) : <p>Loading...</p>}</div>
<button
type="button"
onClick={() =>
addWidget.mutate({
kind: WIDGET_KIND.thread,
title: 'Thread',
content: data.id,
})
}
className="inline-flex min-h-full w-10 shrink-0 items-center justify-center rounded-lg text-neutral-600 hover:text-blue-500 dark:text-neutral-400"
>
<ShareIcon className="h-5 w-5" />
</button>
<div className="flex flex-col gap-2">
<div className="w-full px-3">
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
{thread.rootEventId ? <ChildNote id={thread.rootEventId} /> : null}
</div>
</div>
<button
type="button"
onClick={() =>
addWidget.mutate({
kind: WIDGET_KIND.thread,
title: 'Thread',
content: thread.rootEventId,
})
}
className="self-start text-blue-500 hover:text-blue-600"
>
Show original post
</button>
</div>
</div>
</div>
</div>
);
);
}
if (event.kind === NDKKind.Text) {
return (
<div className="mb-3 h-min w-full px-3">
<div className="flex flex-col gap-2 rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
<div className="flex h-10 items-center justify-between">
<div className="relative flex w-full items-center gap-2 px-3 pt-2">
<div className="absolute -left-0.5 -top-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-blue-500 text-xs ring-2 ring-neutral-50 dark:ring-neutral-950">
<ReplyIcon className="h-4 w-4 text-white" />
</div>
<div className="flex flex-1 items-center justify-between">
<div className="inline-flex items-center gap-1.5">
<User pubkey={event.pubkey} variant="notify" />
<p className="text-neutral-700 dark:text-neutral-300">replied</p>
</div>
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="w-full px-3">
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
{thread.replyEventId ? (
<ChildNote id={thread.replyEventId} />
) : thread.rootEventId ? (
<ChildNote id={thread.rootEventId} isRoot />
) : null}
<button
type="button"
onClick={() =>
addWidget.mutate({
kind: WIDGET_KIND.thread,
title: 'Thread',
content: thread.replyEventId
? thread.replyEventId
: thread.rootEventId,
})
}
className="self-start text-blue-500 hover:text-blue-600"
>
Show full thread
</button>
</div>
</div>
<TextKind content={event.content} textmode />
</div>
</div>
</div>
);
}
}
export const MemoizedNotifyNote = memo(NotifyNote);

View File

@@ -50,7 +50,7 @@ export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) {
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Reply to this post..."
className="h-28 w-full resize-none rounded-t-xl bg-neutral-100 px-5 py-4 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-400"
className="h-28 w-full resize-none rounded-t-xl border-transparent bg-neutral-100 px-5 py-4 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
spellCheck={false}
/>
<div className="inline-flex items-center justify-end gap-2 rounded-b-xl p-2">

View File

@@ -1,10 +1,11 @@
import { NDKUserProfile } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools';
import { useNDK } from '@libs/ndk/provider';
export function useProfile(pubkey: string, embed?: string) {
const queryClient = useQueryClient();
const { ndk } = useNDK();
const {
status,
@@ -13,24 +14,34 @@ export function useProfile(pubkey: string, embed?: string) {
} = useQuery({
queryKey: ['user', pubkey],
queryFn: async () => {
// parse data from nostr.band api
if (embed) {
const profile: NDKUserProfile = JSON.parse(embed);
return profile;
}
// get clean pubkey without any special characters
let hexstring = pubkey.replace(/[^a-zA-Z0-9]/g, '');
if (hexstring.startsWith('npub1'))
hexstring = nip19.decode(hexstring).data as string;
if (hexstring.startsWith('npub1') || hexstring.startsWith('nprofile1')) {
const decoded = nip19.decode(hexstring);
if (decoded.type === 'nprofile') hexstring = decoded.data.pubkey;
if (decoded.type === 'npub') hexstring = decoded.data;
}
const user = ndk.getUser({ pubkey: hexstring });
if (!user) return Promise.reject(new Error("user's profile not found"));
const profile = await user.fetchProfile();
return await user.fetchProfile();
if (!profile)
throw new Error(
`Cannot get metadata for ${pubkey}, will be retry after 10 seconds`
);
return profile;
},
staleTime: Infinity,
refetchOnMount: false,
initialData: () => queryClient.getQueryData(['user', pubkey]),
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: 2,
});
return { status, user, error };