add thread
This commit is contained in:
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -1322,7 +1322,7 @@ packages:
|
|||||||
postcss: ^8.1.0
|
postcss: ^8.1.0
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.21.7
|
browserslist: 4.21.7
|
||||||
caniuse-lite: 1.0.30001498
|
caniuse-lite: 1.0.30001500
|
||||||
fraction.js: 4.2.0
|
fraction.js: 4.2.0
|
||||||
normalize-range: 0.1.2
|
normalize-range: 0.1.2
|
||||||
picocolors: 1.0.0
|
picocolors: 1.0.0
|
||||||
@@ -1364,7 +1364,7 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001498
|
caniuse-lite: 1.0.30001500
|
||||||
electron-to-chromium: 1.4.427
|
electron-to-chromium: 1.4.427
|
||||||
node-releases: 2.0.12
|
node-releases: 2.0.12
|
||||||
update-browserslist-db: 1.0.11(browserslist@4.21.7)
|
update-browserslist-db: 1.0.11(browserslist@4.21.7)
|
||||||
@@ -1414,8 +1414,8 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/caniuse-lite@1.0.30001498:
|
/caniuse-lite@1.0.30001500:
|
||||||
resolution: {integrity: sha512-LFInN2zAwx3ANrGCDZ5AKKJroHqNKyjXitdV5zRIVIaQlXKj3GmxUKagoKsjqUfckpAObPCEWnk5EeMlyMWcgw==}
|
resolution: {integrity: sha512-wSpY0RQnEwFwVZ063ggl3M4ALRP9OSknL0enldDEydIGzuShbtuWwaedB/RfkxsGF3P0kf1Tnv/nTtJEbjzc4Q==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ccount@2.0.1:
|
/ccount@2.0.1:
|
||||||
@@ -4022,8 +4022,8 @@ packages:
|
|||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/rollup@3.24.1:
|
/rollup@3.25.1:
|
||||||
resolution: {integrity: sha512-REHe5dx30ERBRFS0iENPHy+t6wtSEYkjrhwNsLyh3qpRaZ1+aylvMUdMBUHWUD/RjjLmLzEvY8Z9XRlpcdIkHA==}
|
resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==}
|
||||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -4852,7 +4852,7 @@ packages:
|
|||||||
'@types/node': 18.16.17
|
'@types/node': 18.16.17
|
||||||
esbuild: 0.17.19
|
esbuild: 0.17.19
|
||||||
postcss: 8.4.24
|
postcss: 8.4.24
|
||||||
rollup: 3.24.1
|
rollup: 3.25.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ export function ChannelsListItem({ data }: { data: any }) {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800",
|
"inline-flex shrink-0 h-5 w-5 items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800",
|
||||||
pageID === data.event_id
|
pageID === data.event_id
|
||||||
? "dark:bg-zinc-800 group-hover:dark:bg-zinc-700"
|
? "dark:bg-zinc-800 group-hover:dark:bg-zinc-700"
|
||||||
: "",
|
: "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="text-base text-white">#</span>
|
<span className="text-xs text-zinc-100">#</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full inline-flex items-center justify-between">
|
<div className="w-full inline-flex items-center justify-between">
|
||||||
<h5 className="truncate font-medium text-zinc-200">{channel?.name}</h5>
|
<h5 className="truncate font-medium text-zinc-200">{channel?.name}</h5>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { MessageMuteButton } from "@app/channel/components/messages/muteButton";
|
|||||||
import { MessageReplyButton } from "@app/channel/components/messages/replyButton";
|
import { MessageReplyButton } from "@app/channel/components/messages/replyButton";
|
||||||
import { ChannelMessageUser } from "@app/channel/components/messages/user";
|
import { ChannelMessageUser } from "@app/channel/components/messages/user";
|
||||||
import { ChannelMessageUserMute } from "@app/channel/components/messages/userMute";
|
import { ChannelMessageUserMute } from "@app/channel/components/messages/userMute";
|
||||||
import { MentionNote } from "@app/note/components/mentions/note";
|
import { MentionNote } from "@app/space/components/notes/mentions/note";
|
||||||
import { ImagePreview } from "@app/note/components/preview/image";
|
import { ImagePreview } from "@app/space/components/notes/preview/image";
|
||||||
import { VideoPreview } from "@app/note/components/preview/video";
|
import { VideoPreview } from "@app/space/components/notes/preview/video";
|
||||||
import { noteParser } from "@utils/parser";
|
import { noteParser } from "@utils/parser";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ChatMessageUser } from "@app/chat/components/messages/user";
|
import { ChatMessageUser } from "@app/chat/components/messages/user";
|
||||||
import { useDecryptMessage } from "@app/chat/hooks/useDecryptMessage";
|
import { useDecryptMessage } from "@app/chat/hooks/useDecryptMessage";
|
||||||
import { MentionNote } from "@app/note/components/mentions/note";
|
import { MentionNote } from "@app/space/components/notes/mentions/note";
|
||||||
import { ImagePreview } from "@app/note/components/preview/image";
|
import { ImagePreview } from "@app/space/components/notes/preview/image";
|
||||||
import { VideoPreview } from "@app/note/components/preview/video";
|
import { VideoPreview } from "@app/space/components/notes/preview/video";
|
||||||
import { noteParser } from "@utils/parser";
|
import { noteParser } from "@utils/parser";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { LayoutNewsfeed as Layout } from "./layout";
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { NoteReply } from "@app/note/components/metadata/reply";
|
|
||||||
import { NoteRepost } from "@app/note/components/metadata/repost";
|
|
||||||
import { NoteZap } from "@app/note/components/metadata/zap";
|
|
||||||
import { createReplyNote } from "@libs/storage";
|
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import { NDKSubscription } from "@nostr-dev-kit/ndk";
|
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { decode } from "light-bolt11-decoder";
|
|
||||||
import { useContext, useState } from "react";
|
|
||||||
import useSWRSubscription from "swr/subscription";
|
|
||||||
|
|
||||||
export function NoteMetadata({
|
|
||||||
id,
|
|
||||||
eventPubkey,
|
|
||||||
}: {
|
|
||||||
id: string;
|
|
||||||
eventPubkey: string;
|
|
||||||
}) {
|
|
||||||
const ndk = useContext(RelayContext);
|
|
||||||
|
|
||||||
const [replies, setReplies] = useState(0);
|
|
||||||
const [reposts, setReposts] = useState(0);
|
|
||||||
const [zaps, setZaps] = useState(0);
|
|
||||||
|
|
||||||
useSWRSubscription(id ? ["note-metadata", id] : null, () => {
|
|
||||||
const sub: NDKSubscription = ndk.subscribe(
|
|
||||||
{
|
|
||||||
"#e": [id],
|
|
||||||
kinds: [1, 6, 9735],
|
|
||||||
limit: 20,
|
|
||||||
},
|
|
||||||
{ closeOnEose: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
sub.addListener("event", (event: NDKEvent) => {
|
|
||||||
switch (event.kind) {
|
|
||||||
case 1:
|
|
||||||
setReplies((replies) => replies + 1);
|
|
||||||
createReplyNote(
|
|
||||||
event.id,
|
|
||||||
event.pubkey,
|
|
||||||
event.kind,
|
|
||||||
event.tags,
|
|
||||||
event.content,
|
|
||||||
event.created_at,
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
setReposts((reposts) => reposts + 1);
|
|
||||||
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",
|
|
||||||
);
|
|
||||||
setZaps(amount.value / 1000);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
sub.stop();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="inline-flex items-center gap-2 w-full h-12 mt-4">
|
|
||||||
<NoteReply id={id} replies={replies} />
|
|
||||||
<NoteRepost id={id} pubkey={eventPubkey} reposts={reposts} />
|
|
||||||
<div className="ml-auto">
|
|
||||||
<NoteZap zaps={zaps} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { NoteReplyForm } from "@app/note/components/replies/form";
|
|
||||||
import { Reply } from "@app/note/components/replies/item";
|
|
||||||
import { NostrEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
|
||||||
import { sortEvents } from "@utils/transform";
|
|
||||||
import { useContext } from "react";
|
|
||||||
import useSWRSubscription from "swr/subscription";
|
|
||||||
|
|
||||||
export function RepliesList({ id }: { id: string }) {
|
|
||||||
const ndk = useContext(RelayContext);
|
|
||||||
|
|
||||||
const { data, error } = useSWRSubscription(
|
|
||||||
id ? ["note-replies", id] : null,
|
|
||||||
([, key], { next }) => {
|
|
||||||
// subscribe to note
|
|
||||||
const sub = ndk.subscribe(
|
|
||||||
{
|
|
||||||
"#e": [key],
|
|
||||||
kinds: [1],
|
|
||||||
limit: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
closeOnEose: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
sub.addListener("event", (event: NostrEvent) => {
|
|
||||||
next(null, (prev: any) => (prev ? [...prev, event] : [event]));
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
sub.stop();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-5">
|
|
||||||
<div className="mb-2">
|
|
||||||
<h5 className="text-lg font-semibold text-zinc-300">Replies</h5>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<NoteReplyForm id={id} />
|
|
||||||
{error && <div>failed to load</div>}
|
|
||||||
{!data ? (
|
|
||||||
<div className="flex gap-2 px-3 py-4">
|
|
||||||
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800" />
|
|
||||||
<div className="flex w-full flex-1 flex-col justify-center gap-1">
|
|
||||||
<div className="flex items-baseline gap-2 text-base">
|
|
||||||
<div className="h-2.5 w-20 animate-pulse rounded-sm bg-zinc-800" />
|
|
||||||
</div>
|
|
||||||
<div className="h-4 w-44 animate-pulse rounded-sm bg-zinc-800" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
sortEvents(data).map((event: any) => {
|
|
||||||
return <Reply key={event.id} data={event} />;
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { navigate } from "vite-plugin-ssr/client/router";
|
|
||||||
|
|
||||||
export function NoteWrapper({
|
|
||||||
children,
|
|
||||||
href,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
href: string;
|
|
||||||
className: string;
|
|
||||||
}) {
|
|
||||||
const openThread = (event: any, href: string) => {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection.toString().length === 0) {
|
|
||||||
navigate(href, { keepScrollPosition: true });
|
|
||||||
} else {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div className={className}>{children}</div>;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { MultiAccounts } from "@shared/multiAccounts";
|
|
||||||
import { Navigation } from "@shared/navigation";
|
|
||||||
|
|
||||||
export function LayoutNewsfeed({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-screen h-screen">
|
|
||||||
<div className="relative flex flex-row shrink-0">
|
|
||||||
<MultiAccounts />
|
|
||||||
<Navigation />
|
|
||||||
</div>
|
|
||||||
<div className="w-full h-full">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { Kind1 } from "@app/note/components/kind1";
|
|
||||||
import { NoteMetadata } from "@app/note/components/metadata";
|
|
||||||
import { RepliesList } from "@app/note/components/replies/list";
|
|
||||||
import { NoteDefaultUser } from "@app/note/components/user/default";
|
|
||||||
import { getNoteByID } from "@libs/storage";
|
|
||||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
|
||||||
import { noteParser } from "@utils/parser";
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
const fetcher = ([, id]) => getNoteByID(id);
|
|
||||||
|
|
||||||
export function Page() {
|
|
||||||
const pageContext = usePageContext();
|
|
||||||
const searchParams: any = pageContext.urlParsed.search;
|
|
||||||
const noteID = searchParams.id;
|
|
||||||
|
|
||||||
const { data, error } = useSWR(["note", noteID], fetcher);
|
|
||||||
|
|
||||||
const content = !error && data ? noteParser(data) : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="scrollbar-hide h-full w-full overflow-y-auto">
|
|
||||||
<div className="p-3">
|
|
||||||
<div className="relative w-full rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20">
|
|
||||||
{!data || error ? (
|
|
||||||
<div className="animated-pulse p-3">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
|
|
||||||
<div className="flex w-full flex-1 items-start justify-between">
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2 text-base">
|
|
||||||
<div className="h-4 w-16 rounded bg-zinc-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3">
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="h-16 w-full rounded bg-zinc-700" />
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<div className="h-4 w-12 rounded bg-zinc-700" />
|
|
||||||
<div className="h-4 w-12 rounded bg-zinc-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="relative z-10 flex flex-col">
|
|
||||||
<div className="px-3 pt-3">
|
|
||||||
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
|
||||||
<div className="mt-3">
|
|
||||||
<Kind1 content={content} />
|
|
||||||
<NoteMetadata id={noteID} eventPubkey={data.pubkey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<RepliesList id={noteID} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -59,7 +59,6 @@ export function AddBlock() {
|
|||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
{imageModal && <AddImageBlock parentState={setImageModal} />}
|
{imageModal && <AddImageBlock parentState={setImageModal} />}
|
||||||
{feedModal && <AddFeedBlock parentState={setFeedModal} />}
|
{feedModal && <AddFeedBlock parentState={setFeedModal} />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NoteBase } from "@app/note/components/base";
|
import { NoteBase } from "@app/space/components/notes/base";
|
||||||
import { NoteQuoteRepost } from "@app/note/components/quoteRepost";
|
import { NoteQuoteRepost } from "@app/space/components/notes/quoteRepost";
|
||||||
import { NoteSkeleton } from "@app/note/components/skeleton";
|
import { NoteSkeleton } from "@app/space/components/notes/skeleton";
|
||||||
import { getNotesByAuthor } from "@libs/storage";
|
import { getNotesByAuthor } from "@libs/storage";
|
||||||
import { CancelIcon } from "@shared/icons";
|
import { CancelIcon } from "@shared/icons";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
@@ -73,7 +73,7 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 overflow-y-auto"
|
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
|
||||||
style={{ contain: "strict" }}
|
style={{ contain: "strict" }}
|
||||||
>
|
>
|
||||||
{!data || isLoading ? (
|
{!data || isLoading ? (
|
||||||
@@ -108,7 +108,11 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
data-index={virtualRow.index}
|
data-index={virtualRow.index}
|
||||||
ref={rowVirtualizer.measureElement}
|
ref={rowVirtualizer.measureElement}
|
||||||
>
|
>
|
||||||
<NoteBase key={note.event_id} event={note} />
|
<NoteBase
|
||||||
|
key={note.event_id}
|
||||||
|
block={params.id}
|
||||||
|
event={note}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -118,7 +122,11 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
data-index={virtualRow.index}
|
data-index={virtualRow.index}
|
||||||
ref={rowVirtualizer.measureElement}
|
ref={rowVirtualizer.measureElement}
|
||||||
>
|
>
|
||||||
<NoteQuoteRepost key={note.event_id} event={note} />
|
<NoteQuoteRepost
|
||||||
|
key={note.event_id}
|
||||||
|
block={params.id}
|
||||||
|
event={note}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NoteBase } from "@app/note/components/base";
|
import { NoteBase } from "@app/space/components/notes/base";
|
||||||
import { NoteQuoteRepost } from "@app/note/components/quoteRepost";
|
import { NoteQuoteRepost } from "@app/space/components/notes/quoteRepost";
|
||||||
import { NoteSkeleton } from "@app/note/components/skeleton";
|
import { NoteSkeleton } from "@app/space/components/notes/skeleton";
|
||||||
import { getNotes } from "@libs/storage";
|
import { getNotes } from "@libs/storage";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
@@ -11,7 +11,7 @@ const TIME = Math.floor(Date.now() / 1000);
|
|||||||
|
|
||||||
const fetcher = async ([, offset]) => getNotes(TIME, ITEM_PER_PAGE, offset);
|
const fetcher = async ([, offset]) => getNotes(TIME, ITEM_PER_PAGE, offset);
|
||||||
|
|
||||||
export function FollowingBlock() {
|
export function FollowingBlock({ block }: { block: number }) {
|
||||||
const getKey = (pageIndex, previousPageData) => {
|
const getKey = (pageIndex, previousPageData) => {
|
||||||
if (previousPageData && !previousPageData.data) return null;
|
if (previousPageData && !previousPageData.data) return null;
|
||||||
if (pageIndex === 0) return ["following", 0];
|
if (pageIndex === 0) return ["following", 0];
|
||||||
@@ -56,7 +56,7 @@ export function FollowingBlock() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 overflow-y-auto"
|
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
|
||||||
style={{ contain: "strict" }}
|
style={{ contain: "strict" }}
|
||||||
>
|
>
|
||||||
{!data || isLoading ? (
|
{!data || isLoading ? (
|
||||||
@@ -91,7 +91,11 @@ export function FollowingBlock() {
|
|||||||
data-index={virtualRow.index}
|
data-index={virtualRow.index}
|
||||||
ref={rowVirtualizer.measureElement}
|
ref={rowVirtualizer.measureElement}
|
||||||
>
|
>
|
||||||
<NoteBase key={note.event_id} event={note} />
|
<NoteBase
|
||||||
|
key={note.event_id}
|
||||||
|
block={block}
|
||||||
|
event={note}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -101,7 +105,11 @@ export function FollowingBlock() {
|
|||||||
data-index={virtualRow.index}
|
data-index={virtualRow.index}
|
||||||
ref={rowVirtualizer.measureElement}
|
ref={rowVirtualizer.measureElement}
|
||||||
>
|
>
|
||||||
<NoteQuoteRepost key={note.event_id} event={note} />
|
<NoteQuoteRepost
|
||||||
|
key={note.event_id}
|
||||||
|
block={block}
|
||||||
|
event={note}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function ImageBlock({ params }: { params: any }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => close()}
|
onClick={() => close()}
|
||||||
className="inline-flex h-6 w-9 shrink items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800"
|
className="inline-flex h-7 w-7 shrink items-center justify-center rounded bg-zinc-900 hover:bg-zinc-800"
|
||||||
>
|
>
|
||||||
<CancelIcon width={14} height={14} className="text-zinc-500" />
|
<CancelIcon width={14} height={14} className="text-zinc-500" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
65
src/app/space/components/blocks/thread.tsx
Normal file
65
src/app/space/components/blocks/thread.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Kind1 } from "@app/space/components/notes/kind1";
|
||||||
|
import { Kind1063 } from "@app/space/components/notes/kind1063";
|
||||||
|
import { NoteMetadata } from "@app/space/components/notes/metadata";
|
||||||
|
import { RepliesList } from "@app/space/components/notes/replies/list";
|
||||||
|
import { NoteSkeleton } from "@app/space/components/notes/skeleton";
|
||||||
|
import { NoteDefaultUser } from "@app/space/components/user/default";
|
||||||
|
import { getNoteByID } from "@libs/storage";
|
||||||
|
import { ArrowLeftIcon } from "@shared/icons";
|
||||||
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
|
import { noteParser } from "@utils/parser";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const fetcher = ([, id]) => getNoteByID(id);
|
||||||
|
|
||||||
|
export function ThreadBlock({ params }: { params: any }) {
|
||||||
|
const { data } = useSWR(["thread", params.content], fetcher);
|
||||||
|
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
||||||
|
const content = data ? noteParser(data) : null;
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
removeBlock(params.id, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="shrink-0 w-[420px] border-r border-zinc-900">
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => close()}
|
||||||
|
className="inline-flex h-7 w-7 shrink items-center justify-center rounded bg-zinc-900 hover:bg-zinc-800"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon width={14} height={14} className="text-zinc-500" />
|
||||||
|
</button>
|
||||||
|
<h3 className="font-semibold text-zinc-100">{params.title}</h3>
|
||||||
|
<div className="w-9 h-6" />
|
||||||
|
</div>
|
||||||
|
<div className="scrollbar-hide flex w-full h-full flex-col gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
||||||
|
{!data ? (
|
||||||
|
<div className="px-3 py-1.5">
|
||||||
|
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20">
|
||||||
|
<NoteSkeleton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-min w-full px-3 py-1.5">
|
||||||
|
<div className="rounded-md bg-zinc-900 px-5 pt-5">
|
||||||
|
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||||
|
<div className="mt-3">
|
||||||
|
{data.kind === 1 && <Kind1 content={content} />}
|
||||||
|
{data.kind === 1063 && <Kind1063 metadata={data.tags} />}
|
||||||
|
<NoteMetadata id={params.content} eventPubkey={data.pubkey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="px-3">
|
||||||
|
<RepliesList id={params.content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,25 +1,27 @@
|
|||||||
import { Kind1 } from "@app/note/components/kind1";
|
import { Kind1 } from "@app/space/components/notes/kind1";
|
||||||
import { Kind1063 } from "@app/note/components/kind1063";
|
import { Kind1063 } from "@app/space/components/notes/kind1063";
|
||||||
import { NoteMetadata } from "@app/note/components/metadata";
|
import { NoteMetadata } from "@app/space/components/notes/metadata";
|
||||||
import { NoteParent } from "@app/note/components/parent";
|
import { NoteParent } from "@app/space/components/notes/parent";
|
||||||
import { NoteDefaultUser } from "@app/note/components/user/default";
|
import { NoteWrapper } from "@app/space/components/notes/wrapper";
|
||||||
import { NoteWrapper } from "@app/note/components/wrapper";
|
import { NoteDefaultUser } from "@app/space/components/user/default";
|
||||||
|
|
||||||
import { noteParser } from "@utils/parser";
|
import { noteParser } from "@utils/parser";
|
||||||
import { isTagsIncludeID } from "@utils/transform";
|
import { isTagsIncludeID } from "@utils/transform";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function NoteBase({ event }: { event: any }) {
|
export function NoteBase({ block, event }: { block: number; event: any }) {
|
||||||
const content = useMemo(() => noteParser(event), [event]);
|
const content = useMemo(() => noteParser(event), [event]);
|
||||||
const checkParentID = isTagsIncludeID(event.parent_id, event.tags);
|
const checkParentID = isTagsIncludeID(event.parent_id, event.tags);
|
||||||
|
|
||||||
const href = event.parent_id
|
const threadID = event.parent_id ? event.parent_id : event.event_id;
|
||||||
? `/app/note?id=${event.parent_id}`
|
|
||||||
: `/app/note?id=${event.event_id}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoteWrapper href={href} className="h-min w-full px-3 py-1.5">
|
<NoteWrapper
|
||||||
|
thread={threadID}
|
||||||
|
block={block}
|
||||||
|
className="h-min w-full px-3 py-1.5"
|
||||||
|
>
|
||||||
<div className="rounded-md bg-zinc-900 px-5 pt-5">
|
<div className="rounded-md bg-zinc-900 px-5 pt-5">
|
||||||
{event.parent_id &&
|
{event.parent_id &&
|
||||||
(event.parent_id !== event.event_id || checkParentID) ? (
|
(event.parent_id !== event.event_id || checkParentID) ? (
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { LinkPreview } from "./preview/link";
|
import { LinkPreview } from "./preview/link";
|
||||||
import { MentionNote } from "@app/note/components/mentions/note";
|
import { MentionNote } from "@app/space/components/notes/mentions/note";
|
||||||
import { MentionUser } from "@app/note/components/mentions/user";
|
import { MentionUser } from "@app/space/components/notes/mentions/user";
|
||||||
import { ImagePreview } from "@app/note/components/preview/image";
|
import { ImagePreview } from "@app/space/components/notes/preview/image";
|
||||||
import { VideoPreview } from "@app/note/components/preview/video";
|
import { VideoPreview } from "@app/space/components/notes/preview/video";
|
||||||
import { truncateContent } from "@utils/transform";
|
import { truncateContent } from "@utils/transform";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Kind1 } from "@app/note/components/kind1";
|
import { Kind1 } from "@app/space/components/notes/kind1";
|
||||||
import { Kind1063 } from "@app/note/components/kind1063";
|
import { Kind1063 } from "@app/space/components/notes/kind1063";
|
||||||
import { NoteSkeleton } from "@app/note/components/skeleton";
|
import { NoteSkeleton } from "@app/space/components/notes/skeleton";
|
||||||
import { NoteQuoteUser } from "@app/note/components/user/quote";
|
import { NoteWrapper } from "@app/space/components/notes/wrapper";
|
||||||
import { NoteWrapper } from "@app/note/components/wrapper";
|
import { NoteQuoteUser } from "@app/space/components/user/quote";
|
||||||
import { useEvent } from "@utils/hooks/useEvent";
|
import { useEvent } from "@utils/hooks/useEvent";
|
||||||
import { noteParser } from "@utils/parser";
|
import { noteParser } from "@utils/parser";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
113
src/app/space/components/notes/metadata.tsx
Normal file
113
src/app/space/components/notes/metadata.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { NoteReply } from "@app/space/components/notes/metadata/reply";
|
||||||
|
import { NoteRepost } from "@app/space/components/notes/metadata/repost";
|
||||||
|
import { NoteZap } from "@app/space/components/notes/metadata/zap";
|
||||||
|
import { createReplyNote } from "@libs/storage";
|
||||||
|
import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
||||||
|
import { LoaderIcon, ReplyIcon, RepostIcon } from "@shared/icons";
|
||||||
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
|
import { decode } from "light-bolt11-decoder";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const fetcher = async ([, ndk, id]) => {
|
||||||
|
let replies = 0;
|
||||||
|
let reposts = 0;
|
||||||
|
let zap = 0;
|
||||||
|
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
"#e": [id],
|
||||||
|
kinds: [1, 6, 9735],
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = await ndk.fetchEvents(filter);
|
||||||
|
events.forEach((event: NDKEvent) => {
|
||||||
|
switch (event.kind) {
|
||||||
|
case 1:
|
||||||
|
replies += 1;
|
||||||
|
createReplyNote(
|
||||||
|
event.id,
|
||||||
|
event.pubkey,
|
||||||
|
event.kind,
|
||||||
|
event.tags,
|
||||||
|
event.content,
|
||||||
|
event.created_at,
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
reposts += 1;
|
||||||
|
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, reposts, zap };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NoteMetadata({
|
||||||
|
id,
|
||||||
|
eventPubkey,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
eventPubkey: string;
|
||||||
|
}) {
|
||||||
|
const ndk = useContext(RelayContext);
|
||||||
|
const { data } = useSWR(["note-metadata", ndk, id], fetcher);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center gap-2 w-full h-12 mt-4">
|
||||||
|
{!data ? (
|
||||||
|
<>
|
||||||
|
<div className="w-20 group inline-flex items-center gap-1.5">
|
||||||
|
<ReplyIcon
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="text-zinc-400 group-hover:text-green-400"
|
||||||
|
/>
|
||||||
|
<LoaderIcon
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="animate-spin text-black dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-20 group inline-flex items-center gap-1.5">
|
||||||
|
<RepostIcon
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="text-zinc-400 group-hover:text-green-400"
|
||||||
|
/>
|
||||||
|
<LoaderIcon
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="animate-spin text-black dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto">
|
||||||
|
<div className="w-10 h-4 bg-zinc-800 rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<NoteReply id={id} replies={data.replies} />
|
||||||
|
<NoteRepost id={id} pubkey={eventPubkey} reposts={data.reposts} />
|
||||||
|
<div className="ml-auto">
|
||||||
|
<NoteZap zaps={data.zap} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@ import { RelayContext } from "@shared/relayProvider";
|
|||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
import { compactNumber } from "@utils/number";
|
import { compactNumber } from "@utils/number";
|
||||||
import { Fragment, useContext, useEffect, useState } from "react";
|
import { Fragment, useContext, useState } from "react";
|
||||||
|
|
||||||
export function NoteReply({ id, replies }: { id: string; replies: number }) {
|
export function NoteReply({ id, replies }: { id: string; replies: number }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
const account = useActiveAccount((state: any) => state.account);
|
||||||
|
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(replies);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
|
|
||||||
@@ -45,10 +45,6 @@ export function NoteReply({ id, replies }: { id: string; replies: number }) {
|
|||||||
setCount(count + 1);
|
setCount(count + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCount(replies);
|
|
||||||
}, [replies]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -4,7 +4,7 @@ import { RelayContext } from "@shared/relayProvider";
|
|||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
import { compactNumber } from "@utils/number";
|
import { compactNumber } from "@utils/number";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
|
|
||||||
export function NoteRepost({
|
export function NoteRepost({
|
||||||
id,
|
id,
|
||||||
@@ -14,7 +14,7 @@ export function NoteRepost({
|
|||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
const account = useActiveAccount((state: any) => state.account);
|
||||||
|
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(reposts);
|
||||||
|
|
||||||
const submitEvent = (e: any) => {
|
const submitEvent = (e: any) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -40,10 +40,6 @@ export function NoteRepost({
|
|||||||
setCount(count + 1);
|
setCount(count + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCount(reposts);
|
|
||||||
}, [reposts]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
import { compactNumber } from "@utils/number";
|
import { compactNumber } from "@utils/number";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export function NoteZap({ zaps }: { zaps: number }) {
|
export function NoteZap({ zaps }: { zaps: number }) {
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(zaps);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCount(zaps);
|
|
||||||
}, [zaps]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type="button" className="group inline-flex items-center gap-1.5">
|
<button type="button" className="group inline-flex items-center gap-1.5">
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Kind1 } from "@app/note/components/kind1";
|
import { Kind1 } from "@app/space/components/notes/kind1";
|
||||||
import { Kind1063 } from "@app/note/components/kind1063";
|
import { Kind1063 } from "@app/space/components/notes/kind1063";
|
||||||
import { NoteMetadata } from "@app/note/components/metadata";
|
import { NoteMetadata } from "@app/space/components/notes/metadata";
|
||||||
import { NoteSkeleton } from "@app/note/components/skeleton";
|
import { NoteSkeleton } from "@app/space/components/notes/skeleton";
|
||||||
import { NoteDefaultUser } from "@app/note/components/user/default";
|
import { NoteDefaultUser } from "@app/space/components/user/default";
|
||||||
import { useEvent } from "@utils/hooks/useEvent";
|
import { useEvent } from "@utils/hooks/useEvent";
|
||||||
import { noteParser } from "@utils/parser";
|
import { noteParser } from "@utils/parser";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
@@ -9,7 +9,14 @@ export function LinkPreview({ urls }: { urls: string[] }) {
|
|||||||
<div className="mt-3 overflow-hidden rounded-lg bg-zinc-800">
|
<div className="mt-3 overflow-hidden rounded-lg bg-zinc-800">
|
||||||
{error && <p>failed to load</p>}
|
{error && <p>failed to load</p>}
|
||||||
{isLoading || !data ? (
|
{isLoading || !data ? (
|
||||||
<p>Loading...</p>
|
<div className="flex flex-col">
|
||||||
|
<div className="w-full h-16 bg-zinc-700 animate-pulse" />
|
||||||
|
<div className="flex flex-col gap-2 px-3 py-3">
|
||||||
|
<div className="w-2/3 h-3 rounded bg-zinc-700 animate-pulse" />
|
||||||
|
<div className="w-3/4 h-3 rounded bg-zinc-700 animate-pulse" />
|
||||||
|
<div className="mt-2.5 w-1/3 h-2 rounded bg-zinc-700 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
className="flex flex-col"
|
className="flex flex-col"
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
import { RootNote } from "@app/note/components/rootNote";
|
import { RootNote } from "@app/space/components/notes/rootNote";
|
||||||
import { NoteRepostUser } from "@app/note/components/user/repost";
|
import { NoteWrapper } from "@app/space/components/notes/wrapper";
|
||||||
import { NoteWrapper } from "@app/note/components/wrapper";
|
import { NoteRepostUser } from "@app/space/components/user/repost";
|
||||||
import { getQuoteID } from "@utils/transform";
|
import { getQuoteID } from "@utils/transform";
|
||||||
|
|
||||||
export function NoteQuoteRepost({ event }: { event: any }) {
|
export function NoteQuoteRepost({
|
||||||
|
block,
|
||||||
|
event,
|
||||||
|
}: { block: number; event: any }) {
|
||||||
const rootID = getQuoteID(event.tags);
|
const rootID = getQuoteID(event.tags);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoteWrapper
|
<NoteWrapper
|
||||||
href={`/app/note?id=${rootID}`}
|
thread={rootID}
|
||||||
|
block={block}
|
||||||
className="h-min w-full px-3 py-1.5"
|
className="h-min w-full px-3 py-1.5"
|
||||||
>
|
>
|
||||||
<div className="rounded-md bg-zinc-900">
|
<div className="rounded-md bg-zinc-900">
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Kind1 } from "@app/note/components/kind1";
|
import { Kind1 } from "@app/space/components/notes/kind1";
|
||||||
import { NoteMetadata } from "@app/note/components/metadata";
|
import { NoteMetadata } from "@app/space/components/notes/metadata";
|
||||||
import { NoteReplyUser } from "@app/note/components/user/reply";
|
import { NoteReplyUser } from "@app/space/components/user/reply";
|
||||||
import { noteParser } from "@utils/parser";
|
import { noteParser } from "@utils/parser";
|
||||||
|
|
||||||
export function Reply({ data }: { data: any }) {
|
export function Reply({ data }: { data: any }) {
|
||||||
42
src/app/space/components/notes/replies/list.tsx
Normal file
42
src/app/space/components/notes/replies/list.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Reply } from "@app/space/components/notes/replies/item";
|
||||||
|
import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
||||||
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const fetcher = async ([, ndk, id]) => {
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
"#e": [id],
|
||||||
|
kinds: [1],
|
||||||
|
};
|
||||||
|
const events = await ndk.fetchEvents(filter);
|
||||||
|
return [...events];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RepliesList({ id }: { id: string }) {
|
||||||
|
const ndk = useContext(RelayContext);
|
||||||
|
const { data } = useSWR(["note-replies", ndk, id], fetcher);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="mb-2">
|
||||||
|
<h5 className="text-lg font-semibold text-zinc-300">Replies</h5>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{!data ? (
|
||||||
|
<div className="flex gap-2 px-3 py-4">
|
||||||
|
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800" />
|
||||||
|
<div className="flex w-full flex-1 flex-col justify-center gap-1">
|
||||||
|
<div className="flex items-baseline gap-2 text-base">
|
||||||
|
<div className="h-2.5 w-20 animate-pulse rounded-sm bg-zinc-800" />
|
||||||
|
</div>
|
||||||
|
<div className="h-4 w-44 animate-pulse rounded-sm bg-zinc-800" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((event: NDKEvent) => <Reply key={event.id} data={event} />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Kind1 } from "@app/note/components/kind1";
|
import { Kind1 } from "@app/space/components/notes/kind1";
|
||||||
import { Kind1063 } from "@app/note/components/kind1063";
|
import { Kind1063 } from "@app/space/components/notes/kind1063";
|
||||||
import { NoteMetadata } from "@app/note/components/metadata";
|
import { NoteMetadata } from "@app/space/components/notes/metadata";
|
||||||
import { NoteSkeleton } from "@app/note/components/skeleton";
|
import { NoteSkeleton } from "@app/space/components/notes/skeleton";
|
||||||
import { NoteDefaultUser } from "@app/note/components/user/default";
|
import { NoteDefaultUser } from "@app/space/components/user/default";
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { noteParser } from "@utils/parser";
|
import { noteParser } from "@utils/parser";
|
||||||
34
src/app/space/components/notes/wrapper.tsx
Normal file
34
src/app/space/components/notes/wrapper.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
|
|
||||||
|
export function NoteWrapper({
|
||||||
|
children,
|
||||||
|
thread,
|
||||||
|
block,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
thread: string;
|
||||||
|
block: number;
|
||||||
|
className: string;
|
||||||
|
}) {
|
||||||
|
const addTempBlock = useActiveAccount((state: any) => state.addTempBlock);
|
||||||
|
|
||||||
|
const openThread = (event: any, thread: string) => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection.toString().length === 0) {
|
||||||
|
addTempBlock(block, 2, "Thread", thread);
|
||||||
|
} else {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={(e) => openThread(e, thread)}
|
||||||
|
onKeyDown={(e) => openThread(e, thread)}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { AddBlock } from "@app/space/components/add";
|
|||||||
import { FeedBlock } from "@app/space/components/blocks/feed";
|
import { FeedBlock } from "@app/space/components/blocks/feed";
|
||||||
import { FollowingBlock } from "@app/space/components/blocks/following";
|
import { FollowingBlock } from "@app/space/components/blocks/following";
|
||||||
import { ImageBlock } from "@app/space/components/blocks/image";
|
import { ImageBlock } from "@app/space/components/blocks/image";
|
||||||
|
import { ThreadBlock } from "@app/space/components/blocks/thread";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
@@ -16,16 +17,23 @@ export function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
||||||
<FollowingBlock />
|
<FollowingBlock block={1} />
|
||||||
{blocks
|
{!blocks ? (
|
||||||
? blocks.map((block: any) =>
|
<p>Loading...</p>
|
||||||
block.kind === 0 ? (
|
) : (
|
||||||
<ImageBlock key={block.id} params={block} />
|
blocks.map((block: any) => {
|
||||||
) : (
|
switch (block.kind) {
|
||||||
<FeedBlock key={block.id} params={block} />
|
case 0:
|
||||||
),
|
return <ImageBlock key={block.id} params={block} />;
|
||||||
)
|
case 1:
|
||||||
: null}
|
return <FeedBlock key={block.id} params={block} />;
|
||||||
|
case 2:
|
||||||
|
return <ThreadBlock key={block.id} params={block} />;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)}
|
||||||
<div className="shrink-0 w-[90px]">
|
<div className="shrink-0 w-[90px]">
|
||||||
<div className="w-full h-full inline-flex items-center justify-center">
|
<div className="w-full h-full inline-flex items-center justify-center">
|
||||||
<AddBlock />
|
<AddBlock />
|
||||||
|
|||||||
@@ -32,4 +32,5 @@ export * from "./threads";
|
|||||||
export * from "./trash";
|
export * from "./trash";
|
||||||
export * from "./world";
|
export * from "./world";
|
||||||
export * from "./zap";
|
export * from "./zap";
|
||||||
|
export * from "./loader";
|
||||||
// @endindex
|
// @endindex
|
||||||
|
|||||||
29
src/shared/icons/loader.tsx
Normal file
29
src/shared/icons/loader.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
|
export function LoaderIcon(
|
||||||
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<title id="loading">Loading</title>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@ export function Navigation() {
|
|||||||
{/* Chats */}
|
{/* Chats */}
|
||||||
<Disclosure defaultOpen={true}>
|
<Disclosure defaultOpen={true}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div className="flex flex-col gap-0.5 px-1.5">
|
<div className="flex flex-col gap-0.5 px-1.5 pb-6">
|
||||||
<Disclosure.Button className="flex items-center gap-1 px-2.5">
|
<Disclosure.Button className="flex items-center gap-1 px-2.5">
|
||||||
<div
|
<div
|
||||||
className={`inline-flex h-5 w-5 transform items-center justify-center transition-transform duration-150 ease-in-out ${
|
className={`inline-flex h-5 w-5 transform items-center justify-center transition-transform duration-150 ease-in-out ${
|
||||||
|
|||||||
@@ -46,6 +46,27 @@ export const useActiveAccount = create(
|
|||||||
const response = await getBlocks(account.id);
|
const response = await getBlocks(account.id);
|
||||||
set({ blocks: response });
|
set({ blocks: response });
|
||||||
},
|
},
|
||||||
|
addTempBlock: (
|
||||||
|
block: number,
|
||||||
|
kind: number,
|
||||||
|
title: string,
|
||||||
|
content: string,
|
||||||
|
) => {
|
||||||
|
const account = get().account;
|
||||||
|
const target = get().blocks.findIndex(
|
||||||
|
(b: { id: number }) => b.id === block,
|
||||||
|
);
|
||||||
|
// update state
|
||||||
|
set((state: any) => {
|
||||||
|
state.blocks.splice(target, 0, {
|
||||||
|
id: account.id + kind,
|
||||||
|
account_id: account.id,
|
||||||
|
kind,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
addBlock: (kind: number, title: string, content: string) => {
|
addBlock: (kind: number, title: string, content: string) => {
|
||||||
const account = get().account;
|
const account = get().account;
|
||||||
// add to db
|
// add to db
|
||||||
@@ -64,9 +85,11 @@ export const useActiveAccount = create(
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
removeBlock: (id: string) => {
|
removeBlock: (id: string, db?: false) => {
|
||||||
// remove from db
|
if (db) {
|
||||||
removeBlockFromDB(id);
|
// remove from db
|
||||||
|
removeBlockFromDB(id);
|
||||||
|
}
|
||||||
// update state
|
// update state
|
||||||
set((state: any) => {
|
set((state: any) => {
|
||||||
const target = state.blocks.findIndex(
|
const target = state.blocks.findIndex(
|
||||||
|
|||||||
Reference in New Issue
Block a user