mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 11:13:30 +00:00
wip
This commit is contained in:
parent
701712e7b8
commit
ee3e8eb105
@ -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>
|
||||
|
@ -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]
|
||||
|
@ -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>
|
||||
|
@ -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]
|
||||
|
@ -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]
|
||||
|
@ -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',
|
||||
});
|
||||
|
23
src/main.jsx
23
src/main.jsx
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
72
src/shared/notes/article.tsx
Normal file
72
src/shared/notes/article.tsx
Normal 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);
|
@ -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
93
src/shared/notes/file.tsx
Normal 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);
|
@ -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';
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export function Boost({ boost }: { boost: string }) {
|
||||
return <span className="break-words text-blue-400 hover:text-blue-600">{boost}</span>;
|
||||
}
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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
108
src/shared/notes/notify.tsx
Normal 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);
|
@ -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}
|
||||
|
78
src/shared/notes/repost.tsx
Normal file
78
src/shared/notes/repost.tsx
Normal 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
39
src/shared/notes/text.tsx
Normal 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);
|
28
src/shared/notes/unknown.tsx
Normal file
28
src/shared/notes/unknown.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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}>
|
||||
|
@ -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 ? (
|
||||
|
@ -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 ? (
|
||||
|
@ -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>
|
||||
|
@ -13,3 +13,4 @@ export * from './tmp/feeds';
|
||||
export * from './tmp/hashtag';
|
||||
export * from './newsfeed';
|
||||
export * from './notification';
|
||||
export * from './liveUpdater';
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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 ? (
|
||||
|
@ -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>
|
||||
|
@ -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 ? (
|
||||
|
@ -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]
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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(() => {
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user