mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 03:03:31 +00:00
update note
This commit is contained in:
parent
8808cf392f
commit
efea63b0a0
@ -15,27 +15,27 @@
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.23.1",
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@nostr-dev-kit/ndk": "^0.5.1",
|
||||
"@nostr-dev-kit/ndk": "^0.5.2",
|
||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||
"@tauri-apps/api": "^1.4.0",
|
||||
"@vidstack/react": "^0.4.5",
|
||||
"dayjs": "^1.11.8",
|
||||
"destr": "^1.2.2",
|
||||
"get-urls": "^11.0.0",
|
||||
"immer": "^10.0.2",
|
||||
"light-bolt11-decoder": "^3.0.0",
|
||||
"nostr-tools": "^1.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.44.3",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-resizable-panels": "^0.0.48",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-virtuoso": "^4.3.10",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"slate": "^0.94.1",
|
||||
"slate-history": "^0.93.0",
|
||||
"slate-react": "^0.94.2",
|
||||
"swr": "^2.1.5",
|
||||
"tailwind-merge": "^1.13.1",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql",
|
||||
"vidstack": "^0.4.5",
|
||||
"zustand": "^4.3.8"
|
||||
|
1376
pnpm-lock.yaml
1376
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -6,11 +6,11 @@ import { ChannelMessageUserMute } from "@app/channel/components/messages/userMut
|
||||
import { MentionNote } from "@app/space/components/notes/mentions/note";
|
||||
import { ImagePreview } from "@app/space/components/notes/preview/image";
|
||||
import { VideoPreview } from "@app/space/components/notes/preview/video";
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { parser } from "@utils/parser";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export function ChannelMessageItem({ data }: { data: any }) {
|
||||
const content = useMemo(() => noteParser(data), [data]);
|
||||
const content = useMemo(() => parser(data), [data]);
|
||||
const [hide, setHide] = useState(data.hide);
|
||||
|
||||
const toggleHide = () => {
|
||||
|
@ -3,7 +3,7 @@ import { useDecryptMessage } from "@app/chat/hooks/useDecryptMessage";
|
||||
import { MentionNote } from "@app/space/components/notes/mentions/note";
|
||||
import { ImagePreview } from "@app/space/components/notes/preview/image";
|
||||
import { VideoPreview } from "@app/space/components/notes/preview/video";
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { parser } from "@utils/parser";
|
||||
import { memo } from "react";
|
||||
|
||||
export const ChatMessageItem = memo(function ChatMessageItem({
|
||||
@ -21,7 +21,7 @@ export const ChatMessageItem = memo(function ChatMessageItem({
|
||||
data["content"] = decryptedContent;
|
||||
}
|
||||
// parse the note content
|
||||
const content = noteParser(data);
|
||||
const content = parser(data);
|
||||
|
||||
return (
|
||||
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { NoteBase } from "@app/space/components/notes/base";
|
||||
import { NoteQuoteRepost } from "@app/space/components/notes/quoteRepost";
|
||||
import { NoteSkeleton } from "@app/space/components/notes/skeleton";
|
||||
import { getNotes } from "@libs/storage";
|
||||
import { createNote, getNotes } from "@libs/storage";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useContext, useEffect, useMemo, useRef } from "react";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
|
||||
const ITEM_PER_PAGE = 10;
|
||||
const TIME = Math.floor(Date.now() / 1000);
|
||||
@ -12,13 +17,42 @@ const TIME = Math.floor(Date.now() / 1000);
|
||||
const fetcher = async ([, offset]) => getNotes(TIME, ITEM_PER_PAGE, offset);
|
||||
|
||||
export function FollowingBlock({ block }: { block: number }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const account = useActiveAccount((state: any) => state.account);
|
||||
|
||||
const getKey = (pageIndex, previousPageData) => {
|
||||
if (previousPageData && !previousPageData.data) return null;
|
||||
if (pageIndex === 0) return ["following", 0];
|
||||
return ["following", previousPageData.nextCursor];
|
||||
};
|
||||
|
||||
// fetch initial notes
|
||||
const { data, isLoading, size, setSize } = useSWRInfinite(getKey, fetcher);
|
||||
// fetch live notes
|
||||
useSWRSubscription(account ? "eventCollector" : null, () => {
|
||||
const follows = JSON.parse(account.follows);
|
||||
const sub = ndk.subscribe({
|
||||
kinds: [1, 6],
|
||||
authors: follows,
|
||||
since: dateToUnix(),
|
||||
});
|
||||
|
||||
sub.addListener("event", (event: NDKEvent) => {
|
||||
// save note
|
||||
createNote(
|
||||
event.id,
|
||||
event.pubkey,
|
||||
event.kind,
|
||||
event.tags,
|
||||
event.content,
|
||||
event.created_at,
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.stop();
|
||||
};
|
||||
});
|
||||
|
||||
const notes = useMemo(
|
||||
() => (data ? data.flatMap((d) => d.data) : []),
|
||||
@ -26,12 +60,14 @@ export function FollowingBlock({ block }: { block: number }) {
|
||||
);
|
||||
|
||||
const parentRef = useRef();
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: notes.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 400,
|
||||
estimateSize: () => 500,
|
||||
overscan: 2,
|
||||
});
|
||||
|
||||
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
|
||||
|
||||
useEffect(() => {
|
||||
@ -46,6 +82,26 @@ export function FollowingBlock({ block }: { block: number }) {
|
||||
}
|
||||
}, [notes.length, rowVirtualizer.getVirtualItems()]);
|
||||
|
||||
const renderItem = (index: string | number) => {
|
||||
const note = notes[index];
|
||||
|
||||
if (!note) return;
|
||||
|
||||
if (note.kind === 1) {
|
||||
return (
|
||||
<div key={index} data-index={index} ref={rowVirtualizer.measureElement}>
|
||||
<NoteBase block={block} event={note} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={index} data-index={index} ref={rowVirtualizer.measureElement}>
|
||||
<NoteQuoteRepost block={block} event={note} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="shrink-0 w-[420px] border-r border-zinc-900">
|
||||
<div
|
||||
@ -59,7 +115,14 @@ export function FollowingBlock({ block }: { block: number }) {
|
||||
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" }}
|
||||
>
|
||||
{!data || isLoading ? (
|
||||
{isLoading && (
|
||||
<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>
|
||||
)}
|
||||
{!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 />
|
||||
@ -81,40 +144,9 @@ export function FollowingBlock({ block }: { block: number }) {
|
||||
}px)`,
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const note = notes[virtualRow.index];
|
||||
if (note) {
|
||||
if (note.kind === 1) {
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.index}
|
||||
data-index={virtualRow.index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
>
|
||||
<NoteBase
|
||||
key={note.event_id}
|
||||
block={block}
|
||||
event={note}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.index}
|
||||
data-index={virtualRow.index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
>
|
||||
<NoteQuoteRepost
|
||||
key={note.event_id}
|
||||
block={block}
|
||||
event={note}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
})}
|
||||
{rowVirtualizer
|
||||
.getVirtualItems()
|
||||
.map((virtualRow) => renderItem(virtualRow.index))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -8,7 +8,7 @@ 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 { parser } from "@utils/parser";
|
||||
import useSWR from "swr";
|
||||
|
||||
const fetcher = ([, id]) => getNoteByID(id);
|
||||
@ -16,7 +16,7 @@ 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 content = data ? parser(data) : null;
|
||||
|
||||
const close = () => {
|
||||
removeBlock(params.id, false);
|
||||
|
@ -2,26 +2,17 @@ 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 { NoteParent } from "@app/space/components/notes/parent";
|
||||
import { NoteWrapper } from "@app/space/components/notes/wrapper";
|
||||
import { NoteDefaultUser } from "@app/space/components/user/default";
|
||||
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { parser } from "@utils/parser";
|
||||
import { isTagsIncludeID } from "@utils/transform";
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function NoteBase({ block, event }: { block: number; event: any }) {
|
||||
const content = useMemo(() => noteParser(event), [event]);
|
||||
const content = useMemo(() => parser(event), [event]);
|
||||
const checkParentID = isTagsIncludeID(event.parent_id, event.tags);
|
||||
|
||||
const threadID = event.parent_id ? event.parent_id : event.event_id;
|
||||
|
||||
return (
|
||||
<NoteWrapper
|
||||
thread={threadID}
|
||||
block={block}
|
||||
className="h-min w-full px-3 py-1.5"
|
||||
>
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="rounded-md bg-zinc-900 px-5 pt-5">
|
||||
{event.parent_id &&
|
||||
(event.parent_id !== event.event_id || checkParentID) ? (
|
||||
@ -34,10 +25,14 @@ export function NoteBase({ block, event }: { block: number; event: any }) {
|
||||
<div className="-mt-5 pl-[49px]">
|
||||
{event.kind === 1 && <Kind1 content={content} />}
|
||||
{event.kind === 1063 && <Kind1063 metadata={event.tags} />}
|
||||
<NoteMetadata id={event.event_id} eventPubkey={event.pubkey} />
|
||||
<NoteMetadata
|
||||
id={event.event_id}
|
||||
eventPubkey={event.pubkey}
|
||||
currentBlock={block}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NoteWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { LinkPreview } from "./preview/link";
|
||||
import { MentionNote } from "@app/space/components/notes/mentions/note";
|
||||
import { MentionUser } from "@app/space/components/notes/mentions/user";
|
||||
import { ImagePreview } from "@app/space/components/notes/preview/image";
|
||||
import { VideoPreview } from "@app/space/components/notes/preview/video";
|
||||
import { truncateContent } from "@utils/transform";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
export function Kind1({
|
||||
content,
|
||||
@ -13,16 +10,9 @@ export function Kind1({
|
||||
}: { content: any; truncate?: boolean }) {
|
||||
return (
|
||||
<>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[[remarkGfm]]}
|
||||
linkTarget="_blank"
|
||||
className="markdown"
|
||||
components={{
|
||||
em: ({ ...props }) => <MentionUser {...props} />,
|
||||
}}
|
||||
>
|
||||
<div className="select-text whitespace-pre-line break-words text-base leading-tight text-zinc-100">
|
||||
{truncate ? truncateContent(content.parsed, 120) : content.parsed}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
{Array.isArray(content.images) && content.images.length ? (
|
||||
<ImagePreview urls={content.images} />
|
||||
) : (
|
||||
|
@ -4,13 +4,13 @@ import { NoteSkeleton } from "@app/space/components/notes/skeleton";
|
||||
import { NoteWrapper } from "@app/space/components/notes/wrapper";
|
||||
import { NoteQuoteUser } from "@app/space/components/user/quote";
|
||||
import { useEvent } from "@utils/hooks/useEvent";
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { parser } from "@utils/parser";
|
||||
import { memo } from "react";
|
||||
|
||||
export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||
const data = useEvent(id);
|
||||
|
||||
const kind1 = data?.kind === 1 ? noteParser(data) : null;
|
||||
const kind1 = data?.kind === 1 ? parser(data) : null;
|
||||
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
||||
|
||||
return (
|
||||
|
@ -1,20 +1,15 @@
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { shortenKey } from "@utils/shortenKey";
|
||||
|
||||
const hexRegex = /[0-9A-Fa-f]{6}/g;
|
||||
|
||||
export function MentionUser(props: { children: any[] }) {
|
||||
const pubkey = props.children[0].match(hexRegex) ? props.children[0] : null;
|
||||
|
||||
if (!pubkey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function MentionUser({ pubkey }: { pubkey: string }) {
|
||||
const { user } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<span className="text-fuchsia-500">
|
||||
<a
|
||||
href={`/user?pubkey=${pubkey}`}
|
||||
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
|
||||
>
|
||||
@{user?.name || user?.displayName || shortenKey(pubkey)}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
@ -60,20 +60,18 @@ const fetcher = async ([, ndk, id]) => {
|
||||
export function NoteMetadata({
|
||||
id,
|
||||
eventPubkey,
|
||||
currentBlock,
|
||||
}: {
|
||||
id: string;
|
||||
eventPubkey: string;
|
||||
currentBlock?: number;
|
||||
}) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const { data } = useSWR(["note-metadata", ndk, id], fetcher);
|
||||
const { data, isLoading } = useSWR(["note-metadata", ndk, id], fetcher);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-2 w-full h-12"
|
||||
>
|
||||
{!data ? (
|
||||
<div className="inline-flex items-center w-full h-12 mt-4">
|
||||
{!data || isLoading ? (
|
||||
<>
|
||||
<div className="w-20 group inline-flex items-center gap-1.5">
|
||||
<ReplyIcon
|
||||
@ -82,8 +80,8 @@ export function NoteMetadata({
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={16}
|
||||
height={16}
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
@ -94,8 +92,8 @@ export function NoteMetadata({
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={16}
|
||||
height={16}
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
@ -106,15 +104,19 @@ export function NoteMetadata({
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={16}
|
||||
height={16}
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NoteReply id={id} replies={data.replies} />
|
||||
<NoteReply
|
||||
id={id}
|
||||
replies={data.replies}
|
||||
currentBlock={currentBlock}
|
||||
/>
|
||||
<NoteRepost id={id} pubkey={eventPubkey} reposts={data.reposts} />
|
||||
<NoteZap zaps={data.zap} />
|
||||
</>
|
||||
|
@ -1,136 +1,37 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { ReplyIcon } from "@shared/icons";
|
||||
import { Image } from "@shared/image";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { compactNumber } from "@utils/number";
|
||||
import { Fragment, useContext, useState } from "react";
|
||||
|
||||
export function NoteReply({ id, replies }: { id: string; replies: number }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const account = useActiveAccount((state: any) => state.account);
|
||||
export function NoteReply({
|
||||
id,
|
||||
replies,
|
||||
currentBlock,
|
||||
}: { id: string; replies: number; currentBlock?: number }) {
|
||||
const addTempBlock = useActiveAccount((state: any) => state.addTempBlock);
|
||||
|
||||
const [count, setCount] = useState(replies);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const submitEvent = () => {
|
||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||
ndk.signer = signer;
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
// build event
|
||||
event.content = value;
|
||||
event.kind = 1;
|
||||
event.created_at = dateToUnix();
|
||||
event.pubkey = account.pubkey;
|
||||
event.tags = [["e", id]];
|
||||
|
||||
// publish event
|
||||
event.publish();
|
||||
|
||||
// close modal
|
||||
setIsOpen(false);
|
||||
// increment replies
|
||||
setCount(count + 1);
|
||||
const openThread = (event: any, thread: string) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection.toString().length === 0) {
|
||||
addTempBlock(currentBlock, 2, "Thread", thread);
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openModal()}
|
||||
className="w-14 group inline-flex items-center gap-1.5"
|
||||
>
|
||||
<ReplyIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-white">
|
||||
{compactNumber.format(count)}
|
||||
</span>
|
||||
</button>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border border-zinc-800 bg-zinc-900 p-3">
|
||||
{/* root note */}
|
||||
{/* comment form */}
|
||||
<div className="flex gap-2">
|
||||
<div>
|
||||
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
||||
<Image
|
||||
src={account?.image}
|
||||
alt="user's avatar"
|
||||
className="h-11 w-11 rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-24 w-full flex-1 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
||||
<div>
|
||||
<textarea
|
||||
name="content"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="Send your comment"
|
||||
className="relative h-24 w-full resize-none rounded-md border border-black/5 px-3.5 py-3 text-base shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-2 w-full px-2">
|
||||
<div className="flex w-full items-center justify-between bg-zinc-800">
|
||||
<div className="flex items-center gap-2 divide-x divide-zinc-700">
|
||||
<div className="flex items-center gap-2 pl-2" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submitEvent()}
|
||||
disabled={value.length === 0 ? true : false}
|
||||
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-base font-medium shadow-md shadow-fuchsia-900/50 hover:bg-fuchsia-600"
|
||||
>
|
||||
<span className="text-white drop-shadow">Send</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => openThread(e, id)}
|
||||
className="w-14 group inline-flex items-center gap-1.5"
|
||||
>
|
||||
<ReplyIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-white">
|
||||
{compactNumber.format(replies)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { ZapIcon } from "@shared/icons";
|
||||
import { compactNumber } from "@utils/number";
|
||||
import { useState } from "react";
|
||||
|
||||
export function NoteZap({ zaps }: { zaps: number }) {
|
||||
const [count] = useState(zaps);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@ -16,7 +13,7 @@ export function NoteZap({ zaps }: { zaps: number }) {
|
||||
className="text-zinc-400 group-hover:text-blue-400"
|
||||
/>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-white">
|
||||
{compactNumber.format(count)}
|
||||
{compactNumber.format(zaps)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
@ -4,13 +4,13 @@ import { NoteMetadata } from "@app/space/components/notes/metadata";
|
||||
import { NoteSkeleton } from "@app/space/components/notes/skeleton";
|
||||
import { NoteDefaultUser } from "@app/space/components/user/default";
|
||||
import { useEvent } from "@utils/hooks/useEvent";
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { parser } from "@utils/parser";
|
||||
import { memo } from "react";
|
||||
|
||||
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
const data = useEvent(id);
|
||||
|
||||
const kind1 = data?.kind === 1 ? noteParser(data) : null;
|
||||
const kind1 = data?.kind === 1 ? parser(data) : null;
|
||||
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
||||
|
||||
return (
|
||||
|
@ -19,7 +19,7 @@ export function LinkPreview({ urls }: { urls: string[] }) {
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
className="flex flex-col"
|
||||
className="flex flex-col rounded-lg border border-transparent hover:border-fuchsia-900"
|
||||
href={urls[0]}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { RootNote } from "@app/space/components/notes/rootNote";
|
||||
import { NoteWrapper } from "@app/space/components/notes/wrapper";
|
||||
import { NoteRepostUser } from "@app/space/components/user/repost";
|
||||
import { getQuoteID } from "@utils/transform";
|
||||
|
||||
@ -10,18 +9,14 @@ export function NoteQuoteRepost({
|
||||
const rootID = getQuoteID(event.tags);
|
||||
|
||||
return (
|
||||
<NoteWrapper
|
||||
thread={rootID}
|
||||
block={block}
|
||||
className="h-min w-full px-3 py-1.5"
|
||||
>
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="rounded-md bg-zinc-900">
|
||||
<div className="relative px-5 pb-5 pt-5">
|
||||
<div className="absolute left-[35px] top-[20px] h-[70px] w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
|
||||
<NoteRepostUser pubkey={event.pubkey} time={event.created_at} />
|
||||
</div>
|
||||
<RootNote id={rootID} fallback={event.content} />
|
||||
<RootNote id={rootID} fallback={event.content} currentBlock={block} />
|
||||
</div>
|
||||
</NoteWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Kind1 } from "@app/space/components/notes/kind1";
|
||||
import { NoteMetadata } from "@app/space/components/notes/metadata";
|
||||
import { NoteReplyUser } from "@app/space/components/user/reply";
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { parser } from "@utils/parser";
|
||||
|
||||
export function Reply({ data }: { data: any }) {
|
||||
const content = noteParser(data);
|
||||
const content = parser(data);
|
||||
|
||||
return (
|
||||
<div className="flex h-min min-h-min w-full select-text flex-col px-3 pt-5 mb-3 rounded-md bg-zinc-900">
|
||||
|
@ -5,10 +5,9 @@ import { NoteSkeleton } from "@app/space/components/notes/skeleton";
|
||||
import { NoteDefaultUser } from "@app/space/components/user/default";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { parser } from "@utils/parser";
|
||||
import { memo, useContext } from "react";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
import { navigate } from "vite-plugin-ssr/client/router";
|
||||
|
||||
function isJSON(str: string) {
|
||||
try {
|
||||
@ -22,7 +21,8 @@ function isJSON(str: string) {
|
||||
export const RootNote = memo(function RootNote({
|
||||
id,
|
||||
fallback,
|
||||
}: { id: string; fallback?: any }) {
|
||||
currentBlock,
|
||||
}: { id: string; fallback?: any; currentBlock?: number }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const parseFallback = isJSON(fallback) ? JSON.parse(fallback) : null;
|
||||
|
||||
@ -43,27 +43,14 @@ export const RootNote = memo(function RootNote({
|
||||
},
|
||||
);
|
||||
|
||||
const openNote = (e) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection.toString().length === 0) {
|
||||
navigate(`/app/note?id=${id}`);
|
||||
} else {
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const kind1 = !error && data?.kind === 1 ? noteParser(data) : null;
|
||||
const kind1 = !error && data?.kind === 1 ? parser(data) : null;
|
||||
const kind1063 = !error && data?.kind === 1063 ? data.tags : null;
|
||||
|
||||
if (parseFallback) {
|
||||
const contentFallback = noteParser(parseFallback);
|
||||
const contentFallback = parser(parseFallback);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => openNote(e)}
|
||||
onKeyDown={(e) => openNote(e)}
|
||||
className="flex flex-col px-5"
|
||||
>
|
||||
<div className="flex flex-col px-5">
|
||||
<NoteDefaultUser
|
||||
pubkey={parseFallback.pubkey}
|
||||
time={parseFallback.created_at}
|
||||
@ -73,6 +60,7 @@ export const RootNote = memo(function RootNote({
|
||||
<NoteMetadata
|
||||
id={parseFallback.id}
|
||||
eventPubkey={parseFallback.pubkey}
|
||||
currentBlock={currentBlock}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -80,11 +68,7 @@ export const RootNote = memo(function RootNote({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => openNote(e)}
|
||||
onKeyDown={(e) => openNote(e)}
|
||||
className="flex flex-col px-5"
|
||||
>
|
||||
<div className="flex flex-col px-5">
|
||||
{data ? (
|
||||
<>
|
||||
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||
@ -106,7 +90,11 @@ export const RootNote = memo(function RootNote({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<NoteMetadata id={data.id} eventPubkey={data.pubkey} />
|
||||
<NoteMetadata
|
||||
id={data.id}
|
||||
eventPubkey={data.pubkey}
|
||||
currentBlock={currentBlock}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
@ -12,7 +12,10 @@ dayjs.extend(relativeTime);
|
||||
export function NoteDefaultUser({
|
||||
pubkey,
|
||||
time,
|
||||
}: { pubkey: string; time: number }) {
|
||||
}: {
|
||||
pubkey: string;
|
||||
time: number;
|
||||
}) {
|
||||
const { user } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
@ -75,13 +78,13 @@ export function NoteDefaultUser({
|
||||
<div className="flex items-center gap-2 px-3 py-3">
|
||||
<a
|
||||
href={`/app/user?pubkey=${pubkey}`}
|
||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-800 text-base font-medium"
|
||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-800 hover:bg-zinc-700 text-base font-medium"
|
||||
>
|
||||
View full profile
|
||||
</a>
|
||||
<a
|
||||
href={`/app/chat?pubkey=${pubkey}`}
|
||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-800 text-base font-medium"
|
||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-800 hover:bg-zinc-700 text-base font-medium"
|
||||
>
|
||||
Message
|
||||
</a>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { MultiAccounts } from "@shared/multiAccounts";
|
||||
import { Navigation } from "@shared/navigation";
|
||||
import { SWRConfig } from "swr";
|
||||
|
||||
export function LayoutSpace({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@ -9,9 +8,7 @@ export function LayoutSpace({ children }: { children: React.ReactNode }) {
|
||||
<MultiAccounts />
|
||||
<Navigation />
|
||||
</div>
|
||||
<div className="w-full h-full">
|
||||
<SWRConfig value={{ provider: () => new Map() }}>{children}</SWRConfig>
|
||||
</div>
|
||||
<div className="w-full h-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ export function ActiveAccount({ data }: { data: any }) {
|
||||
);
|
||||
|
||||
sub.addListener("event", (event) => {
|
||||
console.log(event);
|
||||
switch (event.kind) {
|
||||
case 4:
|
||||
// save
|
||||
|
@ -33,4 +33,5 @@ export * from "./trash";
|
||||
export * from "./world";
|
||||
export * from "./zap";
|
||||
export * from "./loader";
|
||||
export * from "./trending";
|
||||
// @endindex
|
||||
|
24
src/shared/icons/trending.tsx
Normal file
24
src/shared/icons/trending.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export function TrendingIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M15.75 6.75h5.5v5.5m-.514-4.975L13 15l-4-4-6.25 6.25"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -4,7 +4,12 @@ import { Disclosure } from "@headlessui/react";
|
||||
import { ActiveLink } from "@shared/activeLink";
|
||||
import { AppHeader } from "@shared/appHeader";
|
||||
import { ComposerModal } from "@shared/composer/modal";
|
||||
import { NavArrowDownIcon, SpaceIcon, WorldIcon } from "@shared/icons";
|
||||
import {
|
||||
NavArrowDownIcon,
|
||||
SpaceIcon,
|
||||
TrendingIcon,
|
||||
WorldIcon,
|
||||
} from "@shared/icons";
|
||||
|
||||
export function Navigation() {
|
||||
return (
|
||||
@ -30,7 +35,7 @@ export function Navigation() {
|
||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900">
|
||||
<SpaceIcon width={12} height={12} className="text-white" />
|
||||
</span>
|
||||
<span className="font-medium">Space</span>
|
||||
<span className="font-medium">Spaces</span>
|
||||
</ActiveLink>
|
||||
<ActiveLink
|
||||
href="/app/trending"
|
||||
@ -38,7 +43,7 @@ export function Navigation() {
|
||||
activeClassName="bg-zinc-900/50 hover:bg-zinc-900"
|
||||
>
|
||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900">
|
||||
<WorldIcon width={12} height={12} className="text-white" />
|
||||
<TrendingIcon width={12} height={12} className="text-white" />
|
||||
</span>
|
||||
<span className="font-medium">Trending</span>
|
||||
</ActiveLink>
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { Event, parseReferences } from "nostr-tools";
|
||||
import { MentionUser } from "@app/space/components/notes/mentions/user";
|
||||
import destr from "destr";
|
||||
import getUrls from "get-urls";
|
||||
import { parseReferences } from "nostr-tools";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
|
||||
const getURLs = new RegExp(
|
||||
"(^|[ \t\r\n])((ftp|http|https|gopher|mailto|news|nntp|telnet|wais|file|prospero|aim|webcal|wss|ws):(([A-Za-z0-9$_.+!*(),;/?:@&~=-])|%[A-Fa-f0-9]{2}){2,}(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*(),;/?:@&~=%-]*))?([A-Za-z0-9$_+!*();/?:~-]))",
|
||||
"gmi",
|
||||
);
|
||||
export function parser(event: any) {
|
||||
event.tags = destr(event.tags);
|
||||
|
||||
export function noteParser(event: Event) {
|
||||
const references = parseReferences(event);
|
||||
const urls = getUrls(event.content);
|
||||
|
||||
const content: {
|
||||
original: string;
|
||||
parsed: any;
|
||||
@ -23,11 +26,14 @@ export function noteParser(event: Event) {
|
||||
links: [],
|
||||
};
|
||||
|
||||
// handle media
|
||||
content.original.match(getURLs)?.forEach((item) => {
|
||||
// make sure url is trimmed
|
||||
const url = item.trim();
|
||||
// remove unnecessary whitespaces
|
||||
content.parsed = content.parsed.replace(/\s{2,}/g, " ");
|
||||
|
||||
// remove unnecessary linebreak
|
||||
content.parsed = content.parsed.replace(/(\r\n|\r|\n){2,}/g, "$1\n");
|
||||
|
||||
// parse urls
|
||||
urls?.forEach((url: string) => {
|
||||
if (url.match(/\.(jpg|jpeg|gif|png|webp|avif)$/)) {
|
||||
// image url
|
||||
content.images.push(url);
|
||||
@ -46,18 +52,35 @@ export function noteParser(event: Event) {
|
||||
}
|
||||
});
|
||||
|
||||
// map hashtag to link
|
||||
content.original.match(/#(\w+)(?!:\/\/)/g)?.forEach((item) => {
|
||||
content.parsed = content.parsed.replace(item, `[${item}](/search/${item})`);
|
||||
});
|
||||
|
||||
// handle nostr mention
|
||||
references.forEach((item) => {
|
||||
// parse nostr
|
||||
references?.forEach((item) => {
|
||||
const profile = item.profile;
|
||||
const event = item.event;
|
||||
if (event) {
|
||||
content.notes.push(event.id);
|
||||
content.parsed = reactStringReplace(content.parsed, item.text, () => (
|
||||
<></>
|
||||
));
|
||||
}
|
||||
if (profile) {
|
||||
content.parsed = content.parsed.replace(item.text, `*${profile.pubkey}*`);
|
||||
content.parsed = reactStringReplace(
|
||||
content.parsed,
|
||||
item.text,
|
||||
(match, i) => <MentionUser key={match + i} pubkey={profile.pubkey} />,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// parse hashtag
|
||||
content.parsed = reactStringReplace(content.parsed, /#(\w+)/g, (match, i) => (
|
||||
<a
|
||||
key={match + i}
|
||||
href={`/search/${match}`}
|
||||
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
|
||||
>
|
||||
#{match}
|
||||
</a>
|
||||
));
|
||||
|
||||
return content;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user