This commit is contained in:
reya 2023-11-07 09:35:13 +07:00
parent 701712e7b8
commit ee3e8eb105
42 changed files with 694 additions and 991 deletions

View File

@ -39,10 +39,10 @@ export function ErrorScreen() {
}, []);
return (
<div className="flex h-full items-center justify-center">
<div className="flex h-full items-center justify-center bg-white dark:bg-black">
<div className="flex w-full flex-col gap-4 px-4 md:max-w-lg md:px-0">
<div className="flex flex-col">
<h1 className="mb-1 text-2xl font-semibold text-white">
<h1 className="mb-1 text-2xl font-semibold">
Sorry, an unexpected error has occurred.
</h1>
<div className="mt-4 inline-flex h-16 items-center justify-center rounded-xl border border-dashed border-red-400 bg-red-200/10 px-5">
@ -64,19 +64,19 @@ export function ErrorScreen() {
href="https://github.com/luminous-devs/lume/issues/new"
target="_blank"
rel="noreferrer"
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium"
>
Click here to report the issue on GitHub
</a>
<button
type="button"
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium"
>
Reload app
</button>
<button
type="button"
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium"
>
Reset app
</button>

View File

@ -3,14 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
import { LoaderIcon } from '@shared/icons';
import {
ArticleNote,
FileNote,
NoteWrapper,
Repost,
TextNote,
UnknownNote,
} from '@shared/notes';
import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes';
import { useNostr } from '@utils/hooks/useNostr';
@ -28,31 +21,11 @@ export function UserLatestPosts({ pubkey }: { pubkey: string }) {
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
return <MemoizedTextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <Repost key={event.id} event={event} />;
case 1063:
return (
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
return <MemoizedRepost key={event.id} event={event} />;
default:
return (
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
return <UnknownNote key={event.id} event={event} />;
}
},
[data]

View File

@ -5,7 +5,7 @@ import { useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
import { ArticleDetailNote, NoteActions, NoteReplyForm } from '@shared/notes';
import { NoteActions, NoteReplyForm } from '@shared/notes';
import { ReplyList } from '@shared/notes/replies/list';
import { User } from '@shared/user';
@ -78,9 +78,7 @@ export function ArticleNoteScreen() {
<div className="flex h-min w-full flex-col px-3">
<div className="mb-3 border-b border-neutral-100 pb-3 dark:border-neutral-900">
<User pubkey={data.pubkey} time={data.created_at} variant="thread" />
<div className="mt-3">
<ArticleDetailNote event={data} />
</div>
<div className="mt-3">{data.content}</div>
<div className="mt-3">
<NoteActions id={id} pubkey={data.pubkey} extraButtons={false} />
</div>

View File

@ -6,14 +6,7 @@ import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
import { LoaderIcon } from '@shared/icons';
import {
ArticleNote,
FileNote,
NoteWrapper,
Repost,
TextNote,
UnknownNote,
} from '@shared/notes';
import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes';
export function RelayEventList({ relayUrl }: { relayUrl: string }) {
const { fetcher } = useNDK();
@ -24,9 +17,9 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) {
const events = await fetcher.fetchLatestEvents(
[url],
{
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
kinds: [NDKKind.Text, NDKKind.Repost],
},
100
20
);
return events as unknown as NDKEvent[];
},
@ -37,31 +30,11 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) {
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
return <MemoizedTextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <Repost key={event.id} event={event} />;
case 1063:
return (
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
return <MemoizedRepost key={event.id} event={event} />;
default:
return (
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
return <UnknownNote key={event.id} event={event} />;
}
},
[data]

View File

@ -7,14 +7,7 @@ import { UserProfile } from '@app/users/components/profile';
import { useNDK } from '@libs/ndk/provider';
import {
ArticleNote,
FileNote,
NoteWrapper,
Repost,
TextNote,
UnknownNote,
} from '@shared/notes';
import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes';
export function UserScreen() {
const { pubkey } = useParams();
@ -23,9 +16,9 @@ export function UserScreen() {
queryKey: ['user-feed', pubkey],
queryFn: async () => {
const events = await ndk.fetchEvents({
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Article],
kinds: [NDKKind.Text, NDKKind.Repost],
authors: [pubkey],
limit: 50,
limit: 20,
});
const sorted = [...events].sort((a, b) => b.created_at - a.created_at);
return sorted;
@ -38,31 +31,11 @@ export function UserScreen() {
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
return <MemoizedTextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <Repost key={event.id} event={event} />;
case 1063:
return (
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
return <MemoizedRepost key={event.id} event={event} />;
default:
return (
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
return <UnknownNote key={event.id} event={event} />;
}
},
[data]

View File

@ -77,10 +77,10 @@ export const NDKInstance = () => {
const outboxSetting = await db.getSettingValue('outbox');
const explicitRelayUrls = await getExplicitRelays();
const dexieAdapter = new NDKCacheAdapterTauri(db);
const tauriAdapter = new NDKCacheAdapterTauri(db);
const instance = new NDK({
explicitRelayUrls,
cacheAdapter: dexieAdapter,
cacheAdapter: tauriAdapter,
outboxRelayUrls: ['wss://purplepag.es'],
enableOutboxModel: outboxSetting === '1',
});

View File

@ -1,6 +1,4 @@
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createRoot } from 'react-dom/client';
import { Toaster } from 'sonner';
@ -17,31 +15,16 @@ const queryClient = new QueryClient({
},
});
const persister = createSyncStoragePersister({
storage: window.localStorage,
});
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister,
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
if (query.queryKey === 'widgets') return false;
return true;
},
},
}}
>
<QueryClientProvider client={queryClient}>
<StorageProvider>
<NDKProvider>
<Toaster position="top-center" closeButton />
<App />
</NDKProvider>
</StorageProvider>
</PersistQueryClientProvider>
</QueryClientProvider>
);

View File

@ -25,15 +25,9 @@ export function NoteActions({
return (
<Tooltip.Provider>
<div className="-ml-1 mt-2 inline-flex w-full items-center">
<div className="inline-flex items-center gap-10">
<NoteReply id={id} pubkey={pubkey} root={root} />
<NoteReaction id={id} pubkey={pubkey} />
<NoteRepost id={id} pubkey={pubkey} />
<NoteZap id={id} pubkey={pubkey} />
</div>
<div className="flex h-14 items-center justify-between px-3">
{extraButtons && (
<div className="ml-auto inline-flex items-center gap-3">
<div className="inline-flex items-center gap-3">
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
@ -45,9 +39,10 @@ export function NoteActions({
content: id,
})
}
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-500 dark:text-neutral-300"
className="inline-flex h-7 w-max items-center justify-center gap-2 rounded-full bg-neutral-100 px-2 text-sm font-medium dark:bg-neutral-900"
>
<FocusIcon className="h-5 w-5 group-hover:text-blue-500" />
<FocusIcon className="h-4 w-4" />
Open
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
@ -59,6 +54,12 @@ export function NoteActions({
</Tooltip.Root>
</div>
)}
<div className="inline-flex items-center gap-10">
<NoteReply id={id} pubkey={pubkey} root={root} />
<NoteReaction id={id} pubkey={pubkey} />
<NoteRepost id={id} pubkey={pubkey} />
<NoteZap id={id} pubkey={pubkey} />
</div>
</div>
</Tooltip.Provider>
);

View File

@ -0,0 +1,72 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { memo } from 'react';
import { Link } from 'react-router-dom';
import { User } from '@shared/user';
import { NoteActions } from './actions';
export function ArticleNote({ event }: { event: NDKEvent }) {
const getMetadata = () => {
const title = event.tags.find((tag) => tag[0] === 'title')?.[1];
const image = event.tags.find((tag) => tag[0] === 'image')?.[1];
const summary = event.tags.find((tag) => tag[0] === 'summary')?.[1];
let publishedAt: Date | string | number = event.tags.find(
(tag) => tag[0] === 'published_at'
)?.[1];
if (publishedAt) {
publishedAt = new Date(parseInt(publishedAt) * 1000).toLocaleDateString('en-US');
} else {
publishedAt = new Date(event.created_at * 1000).toLocaleDateString('en-US');
}
return {
title,
image,
publishedAt,
summary,
};
};
const metadata = getMetadata();
return (
<div className="mb-3 h-min w-full px-3">
<div className="relative flex flex-col gap-2 overflow-hidden rounded-xl bg-neutral-50 pt-3 dark:bg-neutral-950">
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
<div>
<Link
to={`/notes/article/${event.id}`}
preventScrollReset={true}
className="flex w-full flex-col rounded-lg border border-neutral-200 bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900"
>
{metadata.image && (
<img
src={metadata.image}
alt={metadata.title}
className="h-56 w-full rounded-t-lg object-cover"
/>
)}
<div className="flex flex-col gap-1 rounded-b-lg bg-neutral-200 px-3 py-3 dark:bg-neutral-800">
<h5 className="break-all font-semibold text-neutral-900 dark:text-neutral-100">
{metadata.title}
</h5>
{metadata.summary ? (
<p className="line-clamp-3 break-all text-sm text-neutral-600 dark:text-neutral-400">
{metadata.summary}
</p>
) : null}
<span className="mt-2.5 text-sm text-neutral-600 dark:text-neutral-400">
{metadata.publishedAt.toString()}
</span>
</div>
</Link>
</div>
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>
);
}
export const MemoizedArticleNote = memo(ArticleNote);

View File

@ -1,90 +1,29 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { nip19 } from 'nostr-tools';
import { useCallback } from 'react';
import {
ArticleNote,
FileNote,
LinkPreview,
NoteActions,
NoteSkeleton,
TextNote,
UnknownNote,
} from '@shared/notes';
import { NoteSkeleton } from '@shared/notes';
import { User } from '@shared/user';
import { useEvent } from '@utils/hooks/useEvent';
export function ChildNote({ id, root }: { id: string; root?: string }) {
export function ChildNote({ id, isRoot }: { id: string; isRoot?: boolean }) {
const { status, data } = useEvent(id);
const renderKind = useCallback(
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote content={event.content} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
return <FileNote event={event} />;
default:
return <UnknownNote event={event} />;
}
},
[id]
);
if (status === 'pending') {
return (
<>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-black/20 to-black/10 dark:from-white/20 dark:to-white/10" />
<div className="relative mb-5 overflow-hidden">
<NoteSkeleton />
</div>
</>
);
}
if (status === 'error') {
const noteLink = `https://njump.me/${nip19.noteEncode(id)}`;
return (
<>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-black/20 to-black/10 dark:from-white/20 dark:to-white/10" />
<div className="relative mb-5 flex flex-col">
<div className="relative z-10 flex items-start gap-3">
<div className="inline-flex h-10 w-10 shrink-0 items-end justify-center rounded-lg bg-black"></div>
<h5 className="truncate font-semibold leading-none text-neutral-900 dark:text-neutral-100">
Lume <span className="text-teal-500">(System)</span>
</h5>
</div>
<div className="-mt-4 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="flex-1">
<div className="prose prose-neutral max-w-none select-text whitespace-pre-line break-all leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-500">
Lume cannot find this post with your current relay set, but you can view
it via njump.me
</div>
<LinkPreview url={noteLink} />
</div>
</div>
</div>
</>
);
return <NoteSkeleton />;
}
return (
<>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.6rem)] w-0.5 bg-gradient-to-t from-black/20 to-black/10 dark:from-white/20 dark:to-white/10" />
<div className="mb-5 flex flex-col">
<User pubkey={data.pubkey} time={data.created_at} eventId={data.id} />
<div className="-mt-4 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="relative z-20 min-w-0 flex-1">
{renderKind(data)}
<NoteActions id={data.id} pubkey={data.pubkey} root={root} />
</div>
<div className="relative flex gap-3">
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-200 dark:bg-neutral-800"></div>
<div className="break-p mt-6 line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
{data.content}
</div>
</div>
</>
<User
pubkey={data.pubkey}
time={data.created_at}
variant="childnote"
subtext={isRoot ? 'posted' : 'replied'}
/>
</div>
);
}

93
src/shared/notes/file.tsx Normal file
View File

@ -0,0 +1,93 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { downloadDir } from '@tauri-apps/api/path';
import { download } from '@tauri-apps/plugin-upload';
import {
MediaControlBar,
MediaController,
MediaFullscreenButton,
MediaMuteButton,
MediaPlayButton,
MediaTimeRange,
} from 'media-chrome/dist/react';
import { memo } from 'react';
import { Link } from 'react-router-dom';
import { DownloadIcon } from '@shared/icons';
import { NoteActions } from '@shared/notes';
import { User } from '@shared/user';
import { fileType } from '@utils/nip94';
export function FileNote({ event }: { event: NDKEvent }) {
const downloadImage = async (url: string) => {
const downloadDirPath = await downloadDir();
const filename = url.substring(url.lastIndexOf('/') + 1);
return await download(url, downloadDirPath + `/${filename}`);
};
const renderFileType = () => {
const url = event.tags.find((el) => el[0] === 'url')[1];
const type = fileType(url);
switch (type) {
case 'image':
return (
<div key={url} className="group">
<img
src={url}
alt={url}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-auto w-full object-cover"
/>
<button
type="button"
onClick={() => downloadImage(url)}
className="absolute right-2 top-2 hidden h-10 w-10 items-center justify-center rounded-lg bg-black/50 backdrop-blur-xl group-hover:inline-flex hover:bg-blue-500"
>
<DownloadIcon className="h-5 w-5 text-white" />
</button>
</div>
);
case 'video':
return (
<MediaController
key={url}
className="aspect-video w-full overflow-hidden rounded-lg"
>
<video slot="media" src={url} preload="metadata" muted />
<MediaControlBar>
<MediaPlayButton></MediaPlayButton>
<MediaTimeRange></MediaTimeRange>
<MediaMuteButton></MediaMuteButton>
<MediaFullscreenButton></MediaFullscreenButton>
</MediaControlBar>
</MediaController>
);
default:
return (
<Link
to={url}
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:text-blue-600"
>
{url}
</Link>
);
}
};
return (
<div className="mb-3 h-min w-full px-3">
<div className="relative flex flex-col gap-2 overflow-hidden rounded-xl bg-neutral-50 pt-3 dark:bg-neutral-950">
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
<div>{renderFileType()}</div>
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>
);
}
export const MemoizedFileNote = memo(FileNote);

View File

@ -1,9 +1,18 @@
export * from './text';
export * from './repost';
export * from './file';
export * from './article';
export * from './child';
export * from './notify';
export * from './unknown';
export * from './skeleton';
export * from './stats';
export * from './actions';
export * from './actions/reaction';
export * from './actions/reply';
export * from './actions/repost';
export * from './actions/zap';
export * from './mentions/note';
export * from './mentions/user';
export * from './actions/more';
export * from './preview/image';
export * from './preview/link';
export * from './preview/video';
@ -11,20 +20,11 @@ export * from './replies/form';
export * from './replies/item';
export * from './replies/list';
export * from './replies/sub';
export * from './kinds/text';
export * from './kinds/file';
export * from './kinds/article';
export * from './kinds/articleDetail';
export * from './kinds/unknown';
export * from './metadata';
export * from './kinds/repost';
export * from './child';
export * from './skeleton';
export * from './actions';
export * from './mentions/hashtag';
export * from './mentions/boost';
export * from './mentions/invoice';
export * from './stats';
export * from './wrapper';
export * from './actions/more';
export * from './replies/replyMediaUploader';
export * from './mentions/note';
export * from './mentions/user';
export * from './mentions/hashtag';
export * from './mentions/invoice';
export * from './kinds/text';
export * from './kinds/article';
export * from './kinds/file';

View File

@ -1,21 +1,18 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { memo, useMemo } from 'react';
import { NDKTag } from '@nostr-dev-kit/ndk';
import { memo } from 'react';
import { Link } from 'react-router-dom';
export function ArticleNote(props: { event?: NDKEvent }) {
const metadata = useMemo(() => {
const title = props.event.tags.find((tag) => tag[0] === 'title')?.[1];
const image = props.event.tags.find((tag) => tag[0] === 'image')?.[1];
const summary = props.event.tags.find((tag) => tag[0] === 'summary')?.[1];
export function ArticleKind({ id, tags }: { id: string; tags: NDKTag[] }) {
const getMetadata = () => {
const title = tags.find((tag) => tag[0] === 'title')?.[1];
const image = tags.find((tag) => tag[0] === 'image')?.[1];
const summary = tags.find((tag) => tag[0] === 'summary')?.[1];
let publishedAt: Date | string | number = props.event.tags.find(
let publishedAt: Date | string | number = tags.find(
(tag) => tag[0] === 'published_at'
)?.[1];
if (publishedAt) {
publishedAt = new Date(parseInt(publishedAt) * 1000).toLocaleDateString('en-US');
} else {
publishedAt = new Date(props.event.created_at * 1000).toLocaleDateString('en-US');
}
publishedAt = new Date(parseInt(publishedAt) * 1000).toLocaleDateString('en-US');
return {
title,
@ -23,23 +20,25 @@ export function ArticleNote(props: { event?: NDKEvent }) {
publishedAt,
summary,
};
}, [props.event.id]);
};
const metadata = getMetadata();
return (
<Link
to={`/notes/article/${props.event.id}`}
to={`/notes/article/${id}`}
preventScrollReset={true}
className="mt-2 flex w-full flex-col rounded-lg border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
className="flex w-full flex-col rounded-lg border border-neutral-200 bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900"
>
{metadata.image && (
<img
src={metadata.image}
alt={metadata.title}
className="h-44 w-full rounded-t-lg object-cover"
className="h-56 w-full rounded-t-lg object-cover"
/>
)}
<div className="flex flex-col gap-1 rounded-b-lg bg-neutral-200 px-3 py-3 dark:bg-neutral-800">
<h5 className="line-clamp-1 font-semibold text-neutral-900 dark:text-neutral-100">
<h5 className="break-all font-semibold text-neutral-900 dark:text-neutral-100">
{metadata.title}
</h5>
{metadata.summary ? (
@ -55,4 +54,4 @@ export function ArticleNote(props: { event?: NDKEvent }) {
);
}
export const MemoizedArticleNote = memo(ArticleNote);
export const MemoizedArticleKind = memo(ArticleKind);

View File

@ -1,38 +0,0 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import Markdown from 'markdown-to-jsx';
import { Boost, Hashtag, Invoice, MentionUser } from '@shared/notes';
export function ArticleDetailNote({ event }: { event: NDKEvent }) {
return (
<Markdown
options={{
overrides: {
Hashtag: {
component: Hashtag,
},
Boost: {
component: Boost,
},
MentionUser: {
component: MentionUser,
},
Invoice: {
component: Invoice,
},
a: {
props: {
target: '_blank',
},
},
},
slugify: (str) => str,
forceBlock: true,
enforceAtxHeadings: true,
}}
className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-500"
>
{event.content}
</Markdown>
);
}

View File

@ -1,4 +1,4 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { NDKTag } from '@nostr-dev-kit/ndk';
import { downloadDir } from '@tauri-apps/api/path';
import { download } from '@tauri-apps/plugin-upload';
import {
@ -10,14 +10,14 @@ import {
MediaTimeRange,
} from 'media-chrome/dist/react';
import { memo } from 'react';
import { Link } from 'react-router-dom';
import { DownloadIcon } from '@shared/icons';
import { LinkPreview } from '@shared/notes';
import { fileType } from '@utils/nip94';
export function FileNote(props: { event?: NDKEvent }) {
const url = props.event.tags.find((el) => el[0] === 'url')[1];
export function FileKind({ tags }: { tags: NDKTag[] }) {
const url = tags.find((el) => el[0] === 'url')[1];
const type = fileType(url);
const downloadImage = async (url: string) => {
@ -28,11 +28,14 @@ export function FileNote(props: { event?: NDKEvent }) {
if (type === 'image') {
return (
<div key={url} className="group relative mt-2">
<div key={url} className="group">
<img
src={url}
alt={url}
className="h-auto w-full rounded-lg border border-neutral-300 object-cover dark:border-neutral-700"
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-auto w-full object-cover"
/>
<button
type="button"
@ -49,7 +52,7 @@ export function FileNote(props: { event?: NDKEvent }) {
return (
<MediaController
key={url}
className="mt-2 aspect-video w-full overflow-hidden rounded-lg"
className="aspect-video w-full overflow-hidden rounded-lg"
>
<video slot="media" src={url} preload="metadata" muted />
<MediaControlBar>
@ -63,10 +66,15 @@ export function FileNote(props: { event?: NDKEvent }) {
}
return (
<div className="mt-2">
<LinkPreview url={url} />
</div>
<Link
to={url}
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:text-blue-600"
>
{url}
</Link>
);
}
export const MemoizedFileNote = memo(FileNote);
export const MemoizedFileKind = memo(FileKind);

View File

@ -1,139 +0,0 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools';
import { memo, useCallback } from 'react';
import { useNDK } from '@libs/ndk/provider';
import {
ArticleNote,
FileNote,
LinkPreview,
NoteActions,
NoteSkeleton,
TextNote,
UnknownNote,
} from '@shared/notes';
import { User } from '@shared/user';
export function Repost({ event }: { event: NDKEvent }) {
const embedEvent: null | NDKEvent =
event.content.length > 0 ? JSON.parse(event.content) : null;
const { ndk } = useNDK();
const { status, isError, data } = useQuery({
queryKey: ['repost', event.id],
queryFn: async () => {
const id = event.tags.find((el) => el[0] === 'e')[1];
if (!id) throw new Error('wrong id');
const ndkEvent = await ndk.fetchEvent(id);
if (!ndkEvent) return Promise.reject(new Error('event not found'));
return ndkEvent;
},
enabled: embedEvent === null,
refetchOnWindowFocus: false,
});
const renderKind = useCallback(
(repostEvent: NDKEvent) => {
switch (repostEvent.kind) {
case NDKKind.Text:
return <TextNote content={repostEvent.content} />;
case NDKKind.Article:
return <ArticleNote event={repostEvent} />;
case 1063:
return <FileNote event={repostEvent} />;
default:
return <UnknownNote event={repostEvent} />;
}
},
[event.id]
);
if (embedEvent) {
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative flex flex-col gap-1 overflow-hidden rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
<User pubkey={event.pubkey} time={event.created_at} variant="repost" />
<div className="relative flex flex-col">
<User
pubkey={embedEvent.pubkey}
time={embedEvent.created_at}
eventId={embedEvent.id}
/>
<div className="-mt-4 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="relative z-20 flex-1">
{renderKind(embedEvent)}
<NoteActions id={embedEvent.id} pubkey={embedEvent.pubkey} />
</div>
</div>
</div>
</div>
</div>
);
}
if (status === 'pending') {
return (
<div className="w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
<NoteSkeleton />
</div>
</div>
);
}
if (isError) {
const noteLink = `https://njump.me/${nip19.noteEncode(
event.tags.find((el) => el[0] === 'e')[1]
)}`;
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
<div className="relative flex flex-col">
<div className="relative z-10 flex items-start gap-3">
<div className="inline-flex h-10 w-10 shrink-0 items-end justify-center rounded-lg bg-black"></div>
<h5 className="truncate font-semibold leading-none text-neutral-900 dark:text-neutral-100">
Lume <span className="text-teal-500">(System)</span>
</h5>
</div>
<div className="-mt-4 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="flex-1">
<div className="prose prose-neutral max-w-none select-text whitespace-pre-line break-all leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-500">
Lume cannot find this post with your current relay set, but you can view
it via njump.me
</div>
<LinkPreview url={noteLink} />
</div>
</div>
</div>
</div>
</div>
);
}
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative flex flex-col gap-1 overflow-hidden rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
<User pubkey={event.pubkey} time={event.created_at} variant="repost" />
<div className="relative flex flex-col">
<User pubkey={data.pubkey} time={data.created_at} eventId={data.id} />
<div className="-mt-4 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="relative z-20 min-w-0 flex-1">
{renderKind(data)}
<NoteActions id={data.id} pubkey={data.pubkey} />
</div>
</div>
</div>
</div>
</div>
);
}
export const MemoizedRepost = memo(Repost);

View File

@ -2,22 +2,24 @@ import { memo } from 'react';
import { useRichContent } from '@utils/hooks/useRichContent';
export function TextNote(props: { content?: string; truncate?: boolean }) {
const { parsedContent } = useRichContent(props.content);
export function TextKind({ content, truncate }: { content: string; truncate?: boolean }) {
const { parsedContent } = useRichContent(content);
if (props.truncate) {
if (truncate) {
return (
<div className="break-p select-text whitespace-pre-line leading-normal text-neutral-900 dark:text-neutral-100">
{props.content}
{content}
</div>
);
}
return (
<div className="break-p select-text whitespace-pre-line leading-normal text-neutral-900 dark:text-neutral-100">
{parsedContent}
<div className={'min-w-0 px-3'}>
<div className="break-p select-text whitespace-pre-line leading-normal text-neutral-900 dark:text-neutral-100">
{parsedContent}
</div>
</div>
);
}
export const MemoizedTextNote = memo(TextNote);
export const MemoizedTextKind = memo(TextKind);

View File

@ -1,19 +0,0 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
export function UnknownNote(props: { event?: NDKEvent }) {
return (
<div className="mt-2 flex w-full flex-col gap-2">
<div className="inline-flex flex-col rounded-md border border-neutral-300 bg-neutral-200 px-2 py-2 dark:border-neutral-700 dark:bg-neutral-800">
<span className="text-sm font-medium text-neutral-500 dark:text-neutral-400">
Kind: {props.event.kind}
</span>
<p className="text-sm text-neutral-800 dark:text-neutral-200">
Unsupport kind on newsfeed
</p>
</div>
<div className="select-text whitespace-pre-line break-words text-neutral-800 dark:text-neutral-200">
{props.event.content.toString()}
</div>
</div>
);
}

View File

@ -1,3 +0,0 @@
export function Boost({ boost }: { boost: string }) {
return <span className="break-words text-blue-400 hover:text-blue-600">{boost}</span>;
}

View File

@ -1,14 +1,12 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { nip19 } from 'nostr-tools';
import { memo } from 'react';
import { PlusIcon } from '@shared/icons';
import {
ArticleNote,
FileNote,
LinkPreview,
MemoizedArticleKind,
MemoizedFileKind,
MemoizedTextKind,
NoteSkeleton,
TextNote,
UnknownNote,
} from '@shared/notes';
import { User } from '@shared/user';
@ -21,66 +19,33 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
const { status, data } = useEvent(id);
const { addWidget } = useWidget();
const openThread = (event, thread: string) => {
const selection = window.getSelection();
if (selection.toString().length === 0) {
addWidget.mutate({
kind: WidgetKinds.local.thread,
title: 'Thread',
content: thread,
});
} else {
event.stopPropagation();
}
};
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote content={event.content} />;
return <MemoizedTextKind content={event.content} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
return <MemoizedArticleKind id={event.id} tags={event.tags} />;
case 1063:
return <FileNote event={event} />;
return <MemoizedFileKind tags={event.tags} />;
default:
return <UnknownNote event={event} />;
return null;
}
};
if (status === 'pending') {
return (
<div className="my-2 w-full cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
<div className="w-full cursor-default rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
<NoteSkeleton />
</div>
);
}
if (status === 'error') {
const noteLink = `https://njump.me/${nip19.noteEncode(id)}`;
return (
<div className="my-2 w-full rounded-lg bg-neutral-200 px-3 py-3 dark:bg-neutral-800">
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 items-end justify-center rounded-md bg-black pb-1"></div>
<h5 className="truncate font-semibold">
Lume <span className="text-green-500">(System)</span>
</h5>
</div>
<div className="mt-1.5">
<LinkPreview url={noteLink} />
</div>
</div>
);
}
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus
<div
role="button"
onClick={(e) => openThread(e, id)}
className="my-2 w-full cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800"
>
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
<div className="mt-1 text-left">{renderKind(data)}</div>
<div className="my-2 flex w-full cursor-default flex-col gap-1 rounded-lg bg-neutral-100 dark:bg-neutral-900">
<div className="px-3 pt-3">
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
</div>
<div>{renderKind(data)}</div>
</div>
);
});

View File

@ -1,104 +0,0 @@
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { decode } from 'light-bolt11-decoder';
import { useNDK } from '@libs/ndk/provider';
import { LoaderIcon } from '@shared/icons';
import { User } from '@shared/user';
import { compactNumber } from '@utils/number';
export function NoteMetadata({ id }: { id: string }) {
const { ndk } = useNDK();
const { status, data } = useQuery({
queryKey: ['note-metadata', id],
queryFn: async () => {
let replies = 0;
let zap = 0;
const users = [];
const filter: NDKFilter = {
'#e': [id],
kinds: [1, 9735],
};
const events = await ndk.fetchEvents(filter);
events.forEach((event: NDKEvent) => {
switch (event.kind) {
case 1:
replies += 1;
if (users.length < 3) users.push(event.pubkey);
break;
case 9735: {
const bolt11 = event.tags.find((tag) => tag[0] === 'bolt11')[1];
if (bolt11) {
const decoded = decode(bolt11);
const amount = decoded.sections.find((item) => item.name === 'amount');
const sats = amount.value / 1000;
zap += sats;
}
break;
}
default:
break;
}
});
return { replies, users, zap };
},
enabled: !!ndk,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
if (status === 'pending') {
return (
<div className="relative z-10 flex items-center gap-3 pb-3">
<div className="mt-2 h-6 w-11 shrink-0"></div>
<div className="mt-2 inline-flex h-6">
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
</div>
</div>
);
}
if (status === 'error') {
<div>
<div className="pb-3" />
</div>;
}
return (
<div>
{data.replies > 0 ? (
<>
<div className="absolute left-[18px] top-14 h-[calc(100%-6.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" />
<div className="relative z-10 flex items-center gap-3 pb-3">
<div className="mt-2 inline-flex h-6 w-11 shrink-0 items-center justify-center">
<div className="isolate flex -space-x-1">
{data.users?.map((user, index) => (
<User key={user + index} pubkey={user} />
))}
</div>
</div>
<div className="mt-2 inline-flex h-6 items-center gap-2">
<button type="button" className="text-neutral-600 dark:text-neutral-400">
<span className="font-semibold text-white">{data.replies}</span> replies
</button>
<span className="text-neutral-600 dark:text-neutral-400">·</span>
<p className="text-neutral-600 dark:text-neutral-400">
<span className="font-semibold text-white">
{compactNumber.format(data.zap)}
</span>{' '}
zaps
</p>
</div>
</div>
</>
) : (
<div className="pb-3" />
)}
</div>
);
}

108
src/shared/notes/notify.tsx Normal file
View File

@ -0,0 +1,108 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { memo } from 'react';
import { ShareIcon } from '@shared/icons';
import { MemoizedArticleKind, MemoizedFileKind, NoteSkeleton } from '@shared/notes';
import { User } from '@shared/user';
import { WidgetKinds } from '@stores/constants';
import { formatCreatedAt } from '@utils/createdAt';
import { useEvent } from '@utils/hooks/useEvent';
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 { addWidget } = useWidget();
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<div className="break-p line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
{event.content}
</div>
);
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 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') {
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>
</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>
</div>
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
</div>
</div>
</div>
<div className="flex gap-2">
<div className="flex-1">{renderKind(data)}</div>
<button
type="button"
onClick={() =>
addWidget.mutate({
kind: WidgetKinds.local.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>
</div>
</div>
);
}
export const MemoizedNotifyNote = memo(NotifyNote);

View File

@ -25,12 +25,25 @@ export function LinkPreview({ url }: { url: string }) {
);
}
if (!data.title && !data.image) {
return (
<Link
to={url}
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:text-blue-600"
>
{url}
</Link>
);
}
return (
<Link
to={url}
target="_blank"
rel="noreferrer"
className="mt-2 flex w-full flex-col rounded-lg border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
className="flex w-full flex-col rounded-lg bg-neutral-100 dark:bg-neutral-900"
>
{isImage(data.image) ? (
<img
@ -41,11 +54,11 @@ export function LinkPreview({ url }: { url: string }) {
) : null}
<div className="flex flex-col items-start px-3 py-3">
<div className="flex flex-col items-start gap-1 text-left">
{data.title && (
<div className="line-clamp-1 text-base font-semibold text-neutral-900 dark:text-neutral-100">
{data.title ? (
<div className="break-all text-base font-semibold text-neutral-900 dark:text-neutral-100">
{data.title}
</div>
)}
) : null}
{data.description ? (
<div className="mb-2 line-clamp-3 break-all text-sm text-neutral-700 dark:text-neutral-400">
{data.description}

View File

@ -0,0 +1,78 @@
import { NDKEvent, NDKKind, NostrEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { memo } from 'react';
import { useNDK } from '@libs/ndk/provider';
import {
MemoizedArticleKind,
MemoizedFileKind,
MemoizedTextKind,
NoteActions,
NoteSkeleton,
} from '@shared/notes';
import { User } from '@shared/user';
export function Repost({ event }: { event: NDKEvent }) {
const { ndk } = useNDK();
const { status, data } = useQuery({
queryKey: ['repost', event.id],
queryFn: async () => {
try {
if (event.content.length > 50) {
const embed = JSON.parse(event.content) as NostrEvent;
const embedEvent = new NDKEvent(ndk, embed);
return embedEvent;
}
const id = event.tags.find((el) => el[0] === 'e')[1];
if (!id) throw new Error('Failed to get repost event id');
const ndkEvent = await ndk.fetchEvent(id);
if (!ndkEvent) return Promise.reject(new Error('Failed to get repost event'));
return ndkEvent;
} catch {
throw new Error('Failed to get repost event');
}
},
refetchOnWindowFocus: false,
});
const renderContentByKind = () => {
if (!data) return null;
switch (data.kind) {
case NDKKind.Text:
return <MemoizedTextKind content={data.content} />;
case 1063:
return <MemoizedFileKind tags={data.tags} />;
case NDKKind.Article:
return <MemoizedArticleKind id={data.id} tags={data.tags} />;
default:
return null;
}
};
if (status === 'pending') {
return (
<div className="w-full px-3 pb-3">
<NoteSkeleton />
</div>
);
}
return (
<div className="mb-3 h-min w-full px-3">
<div className="relative flex flex-col gap-2 overflow-hidden rounded-xl bg-neutral-50 pt-3 dark:bg-neutral-950">
<User pubkey={event.pubkey} time={event.created_at} variant="repost" />
<div className="relative flex flex-col gap-2">
<User pubkey={data.pubkey} time={data.created_at} eventId={data.id} />
{renderContentByKind()}
<NoteActions id={data.id} pubkey={data.pubkey} />
</div>
</div>
</div>
);
}
export const MemoizedRepost = memo(Repost);

39
src/shared/notes/text.tsx Normal file
View File

@ -0,0 +1,39 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { memo } from 'react';
import { ChildNote, NoteActions } from '@shared/notes';
import { User } from '@shared/user';
import { useNostr } from '@utils/hooks/useNostr';
import { useRichContent } from '@utils/hooks/useRichContent';
export function TextNote({ event }: { event: NDKEvent }) {
const { parsedContent } = useRichContent(event.content);
const { getEventThread } = useNostr();
const thread = getEventThread(event);
return (
<div className="mb-3 h-min w-full px-3">
<div className="relative flex flex-col gap-2 overflow-hidden rounded-xl bg-neutral-50 pt-3 dark:bg-neutral-950">
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
{thread ? (
<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} isRoot /> : null}
{thread.replyEventId ? <ChildNote id={thread.replyEventId} /> : null}
</div>
</div>
) : null}
<div className="min-w-0 px-3">
<div className="break-p select-text whitespace-pre-line leading-normal text-neutral-900 dark:text-neutral-100">
{parsedContent}
</div>
</div>
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>
);
}
export const MemoizedTextNote = memo(TextNote);

View File

@ -0,0 +1,28 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { NoteActions } from '@shared/notes';
import { User } from '@shared/user';
export function UnknownNote({ event }: { event: NDKEvent }) {
return (
<div className="mb-3 h-min w-full px-3">
<div className="relative flex flex-col gap-2 overflow-hidden rounded-xl bg-neutral-50 pt-3 dark:bg-neutral-950">
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
<div className="rounded-lg bg-neutral-100 px-2 py-2 dark:bg-neutral-900">
<span className="text-sm font-medium text-neutral-500 dark:text-neutral-400">
Event kind: {event.kind}
</span>
<p className="text-sm text-neutral-800 dark:text-neutral-200">
Unsupported kind
</p>
</div>
<div className="min-w-0 px-3">
<div className="break-p select-text whitespace-pre-line leading-normal text-neutral-900 dark:text-neutral-100">
{event.content.toString()}
</div>
</div>
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>
);
}

View File

@ -1,47 +0,0 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { ReactElement, cloneElement, useMemo } from 'react';
import { ChildNote, NoteActions } from '@shared/notes';
import { User } from '@shared/user';
export function NoteWrapper({
event,
children,
}: {
event: NDKEvent;
children: ReactElement;
}) {
const root = useMemo(() => {
if (event.tags?.[0]?.[0] === 'e' && !event.tags?.[0]?.[3]) {
return event.tags[0][1];
}
return event.tags.find((el) => el[3] === 'root')?.[1];
}, [event.id]);
const reply = useMemo(
() => event.tags.find((el) => el[3] === 'reply')?.[1],
[event.id]
);
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl bg-neutral-100 px-3 py-4 dark:bg-neutral-900">
<div className="relative">{root && <ChildNote id={root} />}</div>
<div className="relative">{reply && <ChildNote id={reply} root={root} />}</div>
<div className="relative flex flex-col">
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
<div className="-mt-4 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="relative z-20 min-w-0 flex-1">
{cloneElement(
children,
event.kind === 1 ? { content: event.content } : { event: event }
)}
<NoteActions id={event.id} pubkey={event.pubkey} root={root} />
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,105 +0,0 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import {
ArticleNote,
FileNote,
NoteSkeleton,
TextNote,
UnknownNote,
} from '@shared/notes';
import { User } from '@shared/user';
import { WidgetKinds } from '@stores/constants';
import { formatCreatedAt } from '@utils/createdAt';
import { useEvent } from '@utils/hooks/useEvent';
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 { addWidget } = useWidget();
const openThread = (event, thread: string) => {
const selection = window.getSelection();
if (selection.toString().length === 0) {
addWidget.mutate({
kind: WidgetKinds.local.thread,
title: 'Thread',
content: thread,
});
} else {
event.stopPropagation();
}
};
const renderKind = (event: NDKEvent) => {
if (!event) return null;
switch (event.kind) {
case NDKKind.Text:
return <TextNote content={event.content} truncate />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
return <FileNote event={event} />;
default:
return <UnknownNote event={event} />;
}
};
const renderText = (kind: number) => {
switch (kind) {
case NDKKind.Text:
return 'replied';
case NDKKind.Reaction:
return `reacted ${event.content}`;
case NDKKind.Repost:
return 'reposted';
case NDKKind.Zap:
return 'zapped';
default:
return 'Unknown';
}
};
if (status === 'pending') {
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>
</div>
);
}
return (
<div className="h-min w-full px-3 pb-3">
<div className="flex flex-col gap-2 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<User pubkey={event.pubkey} variant="notify" />
<p className="font-medium text-neutral-600 dark:text-neutral-400">
{renderText(event.kind)}
</p>
</div>
<span className="text-neutral-500 dark:text-neutral-400">{createdAt}</span>
</div>
{event.kind === 1 ? <TextNote content={event.content} /> : null}
</div>
<div
onClick={(e) => openThread(e, data?.id)}
onKeyDown={(e) => openThread(e, data?.id)}
role="button"
tabIndex={0}
className="cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800"
>
<User pubkey={data?.pubkey} time={data?.created_at} variant="mention" />
<div className="mt-1">{renderKind(data)}</div>
</div>
</div>
</div>
);
}

View File

@ -18,6 +18,7 @@ export const User = memo(function User({
time,
variant = 'default',
embedProfile,
subtext,
}: {
pubkey: string;
eventId?: string;
@ -34,8 +35,10 @@ export const User = memo(function User({
| 'miniavatar'
| 'avatar'
| 'stacked'
| 'ministacked';
| 'ministacked'
| 'childnote';
embedProfile?: string;
subtext?: string;
}) {
const { status, user } = useProfile(pubkey, embedProfile);
@ -63,7 +66,6 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-6 w-6 rounded-md"
/>
<Avatar.Fallback delayMs={300}>
@ -106,7 +108,6 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-8 w-8 rounded-md"
/>
<Avatar.Fallback delayMs={300}>
@ -117,7 +118,7 @@ export const User = memo(function User({
/>
</Avatar.Fallback>
</Avatar.Root>
<h5 className="max-w-[10rem] truncate font-medium text-neutral-900 dark:text-neutral-100">
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
{user?.name ||
user?.display_name ||
user?.displayName ||
@ -149,7 +150,6 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-14 w-14 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>
@ -207,7 +207,6 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-11 w-11 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>
@ -244,7 +243,6 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-12 w-12 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>
@ -261,18 +259,17 @@ export const User = memo(function User({
if (variant === 'miniavatar') {
if (status === 'pending') {
return (
<div className="h-10 w-10 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
);
}
return (
<Avatar.Root>
<Avatar.Root className="h-10 w-10 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-10 w-10 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>
@ -286,6 +283,44 @@ export const User = memo(function User({
);
}
if (variant === 'childnote') {
if (status === 'pending') {
return (
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
);
}
return (
<>
<Avatar.Root className="h-10 w-10 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
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 dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<div className="absolute left-2 top-2 font-semibold leading-tight">
{user?.display_name ||
user?.name ||
user?.displayName ||
displayNpub(pubkey, 16)}{' '}
<span className="font-normal text-neutral-700 dark:text-neutral-300">
{subtext}:
</span>
</div>
</>
);
}
if (variant === 'stacked') {
if (status === 'pending') {
return (
@ -300,7 +335,6 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="inline-block h-8 w-8 rounded-full ring-1 ring-neutral-200 dark:ring-neutral-800"
/>
<Avatar.Fallback delayMs={300}>
@ -328,7 +362,6 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="inline-block h-6 w-6 rounded-full ring-1 ring-white dark:ring-black"
/>
<Avatar.Fallback delayMs={300}>
@ -358,8 +391,8 @@ export const User = memo(function User({
}
return (
<div className="flex gap-3">
<div className="inline-flex h-10 w-10 items-center justify-center">
<div className="flex gap-2 px-3">
<div className="inline-flex w-10 items-center justify-center">
<RepostIcon className="h-5 w-5 text-blue-500" />
</div>
<div className="inline-flex items-center gap-2">
@ -369,7 +402,6 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-6 w-6 rounded"
/>
<Avatar.Fallback delayMs={300}>
@ -415,7 +447,6 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-10 w-10 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>
@ -453,22 +484,21 @@ export const User = memo(function User({
return (
<HoverCard.Root>
<div className="relative z-10 flex items-start gap-3">
<div className="flex items-center gap-3 px-3">
<HoverCard.Trigger asChild>
<Avatar.Root className="relative top-1 h-10 w-10 shrink-0">
<Avatar.Root className="h-9 w-9 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-10 w-10 rounded-lg bg-white object-cover"
className="h-9 w-9 rounded-lg bg-white object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
/>
<Avatar.Fallback delayMs={300}>
<img
src={svgURI}
alt={pubkey}
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
/>
</Avatar.Fallback>
</Avatar.Root>
@ -498,7 +528,6 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-10 w-10 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>

View File

@ -6,7 +6,7 @@ import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { ArticleNote, NoteWrapper } from '@shared/notes';
import { MemoizedArticleNote } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
@ -79,11 +79,7 @@ export function GlobalArticlesWidget({ params }: { params: Widget }) {
</div>
</div>
) : (
allEvents.map((item) => (
<NoteWrapper key={item.id} event={item}>
<ArticleNote />
</NoteWrapper>
))
allEvents.map((item) => <MemoizedArticleNote key={item.id} event={item} />)
)}
<div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (

View File

@ -6,7 +6,7 @@ import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { FileNote, NoteWrapper } from '@shared/notes';
import { MemoizedFileNote } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
@ -79,11 +79,7 @@ export function GlobalFilesWidget({ params }: { params: Widget }) {
</div>
</div>
) : (
allEvents.map((item) => (
<NoteWrapper key={item.id} event={item}>
<FileNote />
</NoteWrapper>
))
allEvents.map((item) => <MemoizedFileNote key={item.id} event={item} />)
)}
<div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (

View File

@ -6,14 +6,7 @@ import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import {
ArticleNote,
FileNote,
NoteWrapper,
Repost,
TextNote,
UnknownNote,
} from '@shared/notes';
import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
@ -37,7 +30,7 @@ export function GlobalHashtagWidget({ params }: { params: Widget }) {
const events = await fetcher.fetchLatestEvents(
relayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
kinds: [NDKKind.Text, NDKKind.Repost],
'#t': [params.content],
},
FETCH_LIMIT,
@ -65,36 +58,19 @@ export function GlobalHashtagWidget({ params }: { params: Widget }) {
);
// render event match event kind
const renderItem = useCallback((event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return <Repost key={event.id} event={event} />;
case 1063:
return (
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
}, []);
const renderItem = useCallback(
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <MemoizedTextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <MemoizedRepost key={event.id} event={event} />;
default:
return <UnknownNote key={event.id} event={event} />;
}
},
[data]
);
return (
<WidgetWrapper>

View File

@ -13,3 +13,4 @@ export * from './tmp/feeds';
export * from './tmp/hashtag';
export * from './newsfeed';
export * from './notification';
export * from './liveUpdater';

View File

@ -1,20 +1,13 @@
import { NDKEvent, NDKFilter, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
import { QueryStatus, useQueryClient } from '@tanstack/react-query';
import { forwardRef, useEffect, useState } from 'react';
import { VListHandle } from 'virtua';
import { useEffect, useState } from 'react';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { ChevronUpIcon } from '@shared/icons';
export const LiveUpdater = forwardRef(function LiveUpdater({
status,
ref,
}: {
status: QueryStatus;
ref: VListHandle;
}) {
export function LiveUpdater({ status }: { status: QueryStatus }) {
const { db } = useStorage();
const { ndk } = useNDK();
@ -32,9 +25,6 @@ export const LiveUpdater = forwardRef(function LiveUpdater({
// reset
setEvents([]);
// scroll to top
ref.scrollToIndex(0, { smooth: true });
};
useEffect(() => {
@ -74,4 +64,4 @@ export const LiveUpdater = forwardRef(function LiveUpdater({
</button>
</div>
);
});
}

View File

@ -7,7 +7,7 @@ import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { ArticleNote, NoteWrapper } from '@shared/notes';
import { MemoizedArticleNote } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
@ -82,11 +82,7 @@ export function LocalArticlesWidget({ params }: { params: Widget }) {
</div>
</div>
) : (
allEvents.map((item) => (
<NoteWrapper key={item.id} event={item}>
<ArticleNote />
</NoteWrapper>
))
allEvents.map((item) => <MemoizedArticleNote key={item.id} event={item} />)
)}
<div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (

View File

@ -7,12 +7,9 @@ import { useNDK } from '@libs/ndk/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import {
MemoizedArticleNote,
MemoizedFileNote,
MemoizedRepost,
MemoizedTextNote,
NoteSkeleton,
NoteWrapper,
UnknownNote,
} from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
@ -38,7 +35,7 @@ export function LocalFeedsWidget({ params }: { params: Widget }) {
const events = await fetcher.fetchLatestEvents(
relayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
kinds: [NDKKind.Text, NDKKind.Repost],
authors: JSON.parse(params.content),
},
FETCH_LIMIT,
@ -65,36 +62,19 @@ export function LocalFeedsWidget({ params }: { params: Widget }) {
[data]
);
const renderItem = useCallback((event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<NoteWrapper key={event.id} event={event}>
<MemoizedTextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return <MemoizedRepost key={event.id} event={event} />;
case 1063:
return (
<NoteWrapper key={event.id} event={event}>
<MemoizedFileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<NoteWrapper key={event.id} event={event}>
<MemoizedArticleNote />
</NoteWrapper>
);
default:
return (
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
}, []);
const renderItem = useCallback(
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <MemoizedTextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <MemoizedRepost key={event.id} event={event} />;
default:
return <UnknownNote key={event.id} event={event} />;
}
},
[data]
);
return (
<WidgetWrapper>

View File

@ -7,7 +7,7 @@ import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { FileNote, NoteWrapper } from '@shared/notes';
import { MemoizedFileNote } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
@ -82,11 +82,7 @@ export function LocalFilesWidget({ params }: { params: Widget }) {
</div>
</div>
) : (
allEvents.map((item) => (
<NoteWrapper key={item.id} event={item}>
<FileNote />
</NoteWrapper>
))
allEvents.map((item) => <MemoizedFileNote key={item.id} event={item} />)
)}
<div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (

View File

@ -6,12 +6,9 @@ import { WVList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
import {
ArticleNote,
FileNote,
MemoizedRepost,
MemoizedTextNote,
NoteSkeleton,
NoteWrapper,
Repost,
TextNote,
UnknownNote,
} from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
@ -30,8 +27,7 @@ export function LocalUserWidget({ params }: { params: Widget }) {
const dedupQueue = new Set();
const events = await ndk.fetchEvents({
// @ts-expect-error, NDK not support file metadata yet
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
kinds: [NDKKind.Text, NDKKind.Repost],
authors: [params.content],
since: nHoursAgo(24),
});
@ -62,31 +58,11 @@ export function LocalUserWidget({ params }: { params: Widget }) {
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
return <MemoizedTextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <Repost key={event.id} event={event} />;
case 1063:
return (
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
return <MemoizedRepost key={event.id} event={event} />;
default:
return (
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
return <UnknownNote key={event.id} event={event} />;
}
},
[data]

View File

@ -11,13 +11,11 @@ import {
MemoizedRepost,
MemoizedTextNote,
NoteSkeleton,
NoteWrapper,
UnknownNote,
} from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { LiveUpdater } from './liveUpdater';
import { LiveUpdater } from '@shared/widgets';
export function NewsfeedWidget() {
const { db } = useStorage();
@ -81,19 +79,11 @@ export function NewsfeedWidget() {
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<NoteWrapper key={event.id} event={event}>
<MemoizedTextNote />
</NoteWrapper>
);
return <MemoizedTextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <MemoizedRepost key={event.id} event={event} />;
default:
return (
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
return <UnknownNote key={event.id} event={event} />;
}
},
[data]
@ -102,7 +92,7 @@ export function NewsfeedWidget() {
return (
<WidgetWrapper>
<TitleBar id="9999" isLive />
<LiveUpdater status={status} ref={ref} />
<LiveUpdater status={status} />
<VList className="flex-1" ref={ref} overscan={2}>
{status === 'pending' ? (
<div className="px-3 py-1.5">

View File

@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { VList } from 'virtua';
import { LoaderIcon } from '@shared/icons';
import { NoteWrapper, TextNote } from '@shared/notes';
import { MemoizedTextNote } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
@ -58,9 +58,7 @@ export function TrendingNotesWidget({ params }: { params: Widget }) {
) : (
<VList className="h-full">
{data.map((item) => (
<NoteWrapper key={item.event.id} event={item.event}>
<TextNote content={item.event.content} />
</NoteWrapper>
<MemoizedTextNote key={item.event.id} event={item.event} />
))}
<div className="h-16" />
</VList>

View File

@ -7,8 +7,7 @@ import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { NoteSkeleton } from '@shared/notes';
import { NotifyNote } from '@shared/notification/notifyNote';
import { MemoizedNotifyNote, NoteSkeleton } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
@ -67,7 +66,7 @@ export function NotificationWidget() {
const renderEvent = useCallback((event: NDKEvent) => {
if (event.pubkey === db.account.pubkey) return null;
return <NotifyNote key={event.id} event={event} />;
return <MemoizedNotifyNote key={event.id} event={event} />;
}, []);
useEffect(() => {

View File

@ -50,6 +50,26 @@ export function useNostr() {
}
};
const getEventThread = (event: NDKEvent) => {
let rootEventId: string;
let replyEventId: string;
if (event.tags?.[0]?.[0] === 'e' && !event.tags?.[0]?.[3]) {
rootEventId = event.tags[0][1];
}
rootEventId = event.tags.find((el) => el[3] === 'root')?.[1] || null;
// eslint-disable-next-line prefer-const
replyEventId = event.tags.find((el) => el[3] === 'reply')?.[1] || null;
if (!rootEventId && !replyEventId) return null;
return {
rootEventId,
replyEventId,
};
};
const getAllActivities = async (limit?: number) => {
try {
const events = await ndk.fetchEvents({
@ -163,36 +183,6 @@ export function useNostr() {
return dedup;
};
const getAllEventsSinceLastLogin = async (customSince?: number) => {
try {
const dbEventsEmpty = await db.isEventsEmpty();
let since: number;
if (!customSince) {
if (dbEventsEmpty || db.account.last_login_at === 0) {
since = db.account.circles.length > 500 ? nHoursAgo(12) : nHoursAgo(24);
} else {
since = db.account.last_login_at;
}
} else {
since = customSince;
}
const events = (await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
authors: db.account.circles,
},
{ since: since }
)) as unknown as NDKEvent[];
return events;
} catch (e) {
console.error('prefetch events failed, error: ', e);
}
};
const getContactsByPubkey = async (pubkey: string) => {
const user = ndk.getUser({ pubkey: pubkey });
const follows = [...(await user.follows())].map((user) => user.hexpubkey);
@ -279,8 +269,8 @@ export function useNostr() {
return {
sub,
getEventThread,
getAllNIP04Chats,
getAllEventsSinceLastLogin,
getContactsByPubkey,
getEventsByPubkey,
getAllRelaysByUsers,