refactor(ark): add note provider
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/react-query": "^5.14.2",
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { LumeStorage } from "@lume/storage";
|
||||
import {
|
||||
type Account,
|
||||
type NDKEventWithReplies,
|
||||
type NIP05,
|
||||
} from "@lume/types";
|
||||
import { type NDKEventWithReplies, type NIP05 } from "@lume/types";
|
||||
import NDK, {
|
||||
NDKEvent,
|
||||
NDKFilter,
|
||||
@@ -26,7 +22,6 @@ export class Ark {
|
||||
#storage: LumeStorage;
|
||||
#fetcher: NostrFetcher;
|
||||
public ndk: NDK;
|
||||
public account: Account;
|
||||
|
||||
constructor({
|
||||
ndk,
|
||||
@@ -36,7 +31,6 @@ export class Ark {
|
||||
}: {
|
||||
ndk: NDK;
|
||||
storage: LumeStorage;
|
||||
|
||||
fetcher: NostrFetcher;
|
||||
}) {
|
||||
this.ndk = ndk;
|
||||
|
||||
@@ -8,11 +8,17 @@ export function ColumnLiveWidget({
|
||||
onClick,
|
||||
}: {
|
||||
filter: NDKFilter;
|
||||
onClick: () => void;
|
||||
onClick: (event: NDKEvent[]) => void;
|
||||
}) {
|
||||
const ark = useArk();
|
||||
const [events, setEvents] = useState<NDKEvent[]>([]);
|
||||
|
||||
const update = async () => {
|
||||
onClick(events);
|
||||
// reset
|
||||
setEvents([]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const sub = ark.subscribe({
|
||||
filter,
|
||||
@@ -31,7 +37,7 @@ export function ColumnLiveWidget({
|
||||
<div className="absolute left-0 top-11 z-50 flex h-11 w-full items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
onClick={update}
|
||||
className="inline-flex h-9 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-2.5 text-sm font-semibold text-white hover:bg-blue-600"
|
||||
>
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
|
||||
22
packages/ark/src/components/note/builds/childReply.tsx
Normal file
22
packages/ark/src/components/note/builds/childReply.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { Note } from "..";
|
||||
|
||||
export function ChildReply({
|
||||
event,
|
||||
rootEventId,
|
||||
}: { event: NDKEvent; rootEventId?: string }) {
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root>
|
||||
<Note.User />
|
||||
<Note.TextContent content={event.content} className="min-w-0" />
|
||||
<div className="-ml-1 flex h-14 items-center gap-10">
|
||||
<Note.Reply rootEventId={rootEventId} />
|
||||
<Note.Reaction />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Note } from "..";
|
||||
import { ChildReply } from "./childReply";
|
||||
|
||||
export function Reply({
|
||||
event,
|
||||
@@ -16,58 +17,44 @@ export function Reply({
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
<Note.Root>
|
||||
<Note.User
|
||||
pubkey={event.pubkey}
|
||||
time={event.created_at}
|
||||
className="h-14 px-3"
|
||||
/>
|
||||
<Note.TextContent content={event.content} className="min-w-0 px-3" />
|
||||
<div className="-ml-1 flex items-center justify-between h-14 px-3">
|
||||
{event.replies?.length > 0 ? (
|
||||
<Collapsible.Trigger asChild>
|
||||
<div className="ml-1 inline-flex h-14 items-center gap-1 font-semibold text-blue-500">
|
||||
<NavArrowDownIcon
|
||||
className={twMerge(
|
||||
"h-3 w-3",
|
||||
open ? "rotate-180 transform" : "",
|
||||
)}
|
||||
/>
|
||||
{`${event.replies?.length} ${
|
||||
event.replies?.length === 1 ? "reply" : "replies"
|
||||
}`}
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
) : null}
|
||||
<div className="inline-flex items-center gap-10">
|
||||
<Note.Reply eventId={event.id} rootEventId={rootEvent} />
|
||||
<Note.Reaction event={event} />
|
||||
<Note.Repost event={event} />
|
||||
<Note.Zap event={event} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={twMerge("px-3", open ? "pb-3" : "")}>
|
||||
{event.replies?.length > 0 ? (
|
||||
<Collapsible.Content>
|
||||
{event.replies?.map((childEvent) => (
|
||||
<Note.Root key={childEvent.id}>
|
||||
<Note.User pubkey={event.pubkey} time={event.created_at} />
|
||||
<Note.TextContent
|
||||
content={event.content}
|
||||
className="min-w-0"
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root>
|
||||
<Note.User className="h-14 px-3" />
|
||||
<Note.TextContent content={event.content} className="min-w-0 px-3" />
|
||||
<div className="-ml-1 flex items-center justify-between h-14 px-3">
|
||||
{event.replies?.length > 0 ? (
|
||||
<Collapsible.Trigger asChild>
|
||||
<div className="ml-1 inline-flex h-14 items-center gap-1 font-semibold text-blue-500">
|
||||
<NavArrowDownIcon
|
||||
className={twMerge(
|
||||
"h-3 w-3",
|
||||
open ? "rotate-180 transform" : "",
|
||||
)}
|
||||
/>
|
||||
<div className="-ml-1 flex h-14 items-center gap-10">
|
||||
<Note.Reply eventId={event.id} rootEventId={rootEvent} />
|
||||
<Note.Reaction event={event} />
|
||||
<Note.Repost event={event} />
|
||||
<Note.Zap event={event} />
|
||||
</div>
|
||||
</Note.Root>
|
||||
))}
|
||||
</Collapsible.Content>
|
||||
) : null}
|
||||
</div>
|
||||
</Note.Root>
|
||||
{`${event.replies?.length} ${
|
||||
event.replies?.length === 1 ? "reply" : "replies"
|
||||
}`}
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
) : null}
|
||||
<div className="inline-flex items-center gap-10">
|
||||
<Note.Reply rootEventId={rootEvent} />
|
||||
<Note.Reaction />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
</div>
|
||||
<div className={twMerge("px-3", open ? "pb-3" : "")}>
|
||||
{event.replies?.length > 0 ? (
|
||||
<Collapsible.Content>
|
||||
{event.replies?.map((childEvent) => (
|
||||
<ChildReply key={childEvent.id} event={childEvent} />
|
||||
))}
|
||||
</Collapsible.Content>
|
||||
) : null}
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { Note } from "..";
|
||||
import { useArk } from "../../../provider";
|
||||
|
||||
export function RepostNote({ event }: { event: NDKEvent }) {
|
||||
export function RepostNote({
|
||||
event,
|
||||
className,
|
||||
}: { event: NDKEvent; className?: string }) {
|
||||
const ark = useArk();
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
@@ -57,26 +61,23 @@ export function RepostNote({ event }: { event: NDKEvent }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Note.Root>
|
||||
<Note.User
|
||||
pubkey={event.pubkey}
|
||||
time={event.created_at}
|
||||
variant="repost"
|
||||
className="h-14"
|
||||
/>
|
||||
<div className="relative flex flex-col gap-2 px-3">
|
||||
<Note.User pubkey={repostEvent.pubkey} time={repostEvent.created_at} />
|
||||
{renderContentByKind()}
|
||||
<div className="flex h-14 items-center justify-between">
|
||||
<Note.Pin eventId={event.id} />
|
||||
<div className="inline-flex items-center gap-10">
|
||||
<Note.Reply eventId={repostEvent.id} />
|
||||
<Note.Reaction event={repostEvent} />
|
||||
<Note.Repost event={repostEvent} />
|
||||
<Note.Zap event={repostEvent} />
|
||||
<Note.Provider event={repostEvent}>
|
||||
<Note.Root className={className}>
|
||||
<Note.User variant="repost" className="h-14" />
|
||||
<div className="relative flex flex-col gap-2 px-3">
|
||||
<Note.User />
|
||||
{renderContentByKind()}
|
||||
<div className="flex h-14 items-center justify-between">
|
||||
<Note.Pin />
|
||||
<div className="inline-flex items-center gap-10">
|
||||
<Note.Reply />
|
||||
<Note.Reaction />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,23 +10,24 @@ export function TextNote({
|
||||
const thread = ark.getEventThread({ tags: event.tags });
|
||||
|
||||
return (
|
||||
<Note.Root className={className}>
|
||||
<Note.User
|
||||
pubkey={event.pubkey}
|
||||
time={event.created_at}
|
||||
className="h-14 px-3"
|
||||
/>
|
||||
<Note.Thread thread={thread} className="mb-2" />
|
||||
<Note.TextContent content={event.content} className="min-w-0 px-3" />
|
||||
<div className="flex h-14 items-center justify-between px-3">
|
||||
<Note.Pin eventId={event.id} />
|
||||
<div className="inline-flex items-center gap-10">
|
||||
<Note.Reply eventId={event.id} rootEventId={thread?.rootEventId} />
|
||||
<Note.Reaction event={event} />
|
||||
<Note.Repost event={event} />
|
||||
<Note.Zap event={event} />
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root className={className}>
|
||||
<div className="h-14 px-3 flex items-center justify-between">
|
||||
<Note.User className="flex-1 pr-1" />
|
||||
<Note.Menu />
|
||||
</div>
|
||||
</div>
|
||||
</Note.Root>
|
||||
<Note.Thread thread={thread} className="mb-2" />
|
||||
<Note.TextContent content={event.content} className="min-w-0 px-3" />
|
||||
<div className="flex h-14 items-center justify-between px-3">
|
||||
<Note.Pin />
|
||||
<div className="inline-flex items-center gap-10">
|
||||
<Note.Reply rootEventId={thread?.rootEventId} />
|
||||
<Note.Reaction />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,23 +33,20 @@ export function ThreadNote({ eventId }: { eventId: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Note.Root>
|
||||
<Note.User
|
||||
pubkey={data.pubkey}
|
||||
time={data.created_at}
|
||||
variant="thread"
|
||||
className="h-16 px-3"
|
||||
/>
|
||||
{renderEventKind(data)}
|
||||
<div className="flex h-14 items-center justify-between px-3">
|
||||
<Note.Pin eventId={data.id} />
|
||||
<div className="inline-flex items-center gap-10">
|
||||
<Note.Reply eventId={data.id} />
|
||||
<Note.Reaction event={data} />
|
||||
<Note.Repost event={data} />
|
||||
<Note.Zap event={data} />
|
||||
<Note.Provider event={data}>
|
||||
<Note.Root>
|
||||
<Note.User variant="thread" className="h-16 px-3" />
|
||||
{renderEventKind(data)}
|
||||
<div className="flex h-14 items-center justify-between px-3">
|
||||
<Note.Pin />
|
||||
<div className="inline-flex items-center gap-10">
|
||||
<Note.Reply />
|
||||
<Note.Reaction />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { PinIcon } from "@lume/icons";
|
||||
import { WIDGET_KIND } from "@lume/utils";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { useNoteContext } from "..";
|
||||
import { useWidget } from "../../../hooks/useWidget";
|
||||
|
||||
export function NotePin({ eventId }: { eventId: string }) {
|
||||
export function NotePin() {
|
||||
const event = useNoteContext();
|
||||
const { addWidget } = useWidget();
|
||||
|
||||
return (
|
||||
@@ -16,19 +18,19 @@ export function NotePin({ eventId }: { eventId: string }) {
|
||||
addWidget.mutate({
|
||||
kind: WIDGET_KIND.thread,
|
||||
title: "Thread",
|
||||
content: eventId,
|
||||
content: event.id,
|
||||
})
|
||||
}
|
||||
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"
|
||||
className="inline-flex h-7 w-max items-center justify-center gap-2 rounded-full bg-neutral-100 hover:bg-neutral-200 dark:hover:bg-neutral-800 px-2 text-sm font-medium dark:bg-neutral-900"
|
||||
>
|
||||
<PinIcon className="size-4" />
|
||||
Pin
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content className="-left-10 inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-200 px-3.5 text-sm text-neutral-900 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-800 dark:text-neutral-100">
|
||||
<Tooltip.Content className="bg-neutral-950 text-white -left-10 inline-flex h-7 select-none items-center justify-center rounded-md px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
||||
Pin note
|
||||
<Tooltip.Arrow className="fill-neutral-200 dark:fill-neutral-800" />
|
||||
<Tooltip.Arrow className="fill-neutral-950" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ReactionIcon } from "@lume/icons";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import * as HoverCard from "@radix-ui/react-hover-card";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
const REACTIONS = [
|
||||
{
|
||||
@@ -27,7 +27,9 @@ const REACTIONS = [
|
||||
},
|
||||
];
|
||||
|
||||
export function NoteReaction({ event }: { event: NDKEvent }) {
|
||||
export function NoteReaction() {
|
||||
const event = useNoteContext();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [reaction, setReaction] = useState<string | null>(null);
|
||||
|
||||
@@ -52,8 +54,8 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<HoverCard.Root open={open} onOpenChange={setOpen}>
|
||||
<HoverCard.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
|
||||
@@ -62,16 +64,16 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
|
||||
<img
|
||||
src={getReactionImage(reaction)}
|
||||
alt={reaction}
|
||||
className="h-5 w-5"
|
||||
className="size-5"
|
||||
/>
|
||||
) : (
|
||||
<ReactionIcon className="h-5 w-5 group-hover:text-blue-500" />
|
||||
<ReactionIcon className="size-5 group-hover:text-blue-500" />
|
||||
)}
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
className="select-none rounded-md bg-neutral-200 px-1 py-1 text-sm will-change-[transform,opacity] data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-800"
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content
|
||||
className="select-none rounded-lg bg-neutral-950 px-1 py-1 text-sm will-change-[transform,opacity] data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=top]:animate-slideDownAndFade"
|
||||
sideOffset={0}
|
||||
side="top"
|
||||
>
|
||||
@@ -79,7 +81,7 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => react("👏")}
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded backdrop-blur-xl hover:bg-white/10"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"
|
||||
>
|
||||
<img
|
||||
src="/clapping_hands.png"
|
||||
@@ -90,7 +92,7 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => react("🤪")}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded backdrop-blur-xl hover:bg-white/10"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"
|
||||
>
|
||||
<img
|
||||
src="/face_with_tongue.png"
|
||||
@@ -101,7 +103,7 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => react("😮")}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded backdrop-blur-xl hover:bg-white/10"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"
|
||||
>
|
||||
<img
|
||||
src="/face_with_open_mouth.png"
|
||||
@@ -112,7 +114,7 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => react("😢")}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded backdrop-blur-xl hover:bg-white/10"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"
|
||||
>
|
||||
<img
|
||||
src="/crying_face.png"
|
||||
@@ -123,14 +125,14 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => react("🤡")}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded backdrop-blur-xl hover:bg-white/10"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"
|
||||
>
|
||||
<img src="/clown_face.png" alt="Clown Face" className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<Popover.Arrow className="fill-neutral-200 dark:fill-neutral-800" />
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
<HoverCard.Arrow className="fill-neutral-950" />
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Portal>
|
||||
</HoverCard.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ReplyIcon } from "@lume/icons";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { createSearchParams, useNavigate } from "react-router-dom";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteReply({
|
||||
eventId,
|
||||
rootEventId,
|
||||
}: {
|
||||
eventId: string;
|
||||
rootEventId?: string;
|
||||
}) {
|
||||
const event = useNoteContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
@@ -21,7 +21,7 @@ export function NoteReply({
|
||||
navigate({
|
||||
pathname: "/new/",
|
||||
search: createSearchParams({
|
||||
replyTo: eventId,
|
||||
replyTo: event.id,
|
||||
rootReplyTo: rootEventId,
|
||||
}).toString(),
|
||||
})
|
||||
@@ -32,9 +32,9 @@ export function NoteReply({
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content className="-left-10 inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-200 px-3.5 text-sm text-neutral-900 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-800 dark:text-neutral-100">
|
||||
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-white will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
||||
Quick reply
|
||||
<Tooltip.Arrow className="fill-neutral-200 dark:fill-neutral-800" />
|
||||
<Tooltip.Arrow className="fill-neutral-950" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { RepostIcon } from "@lume/icons";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteRepost({ event }: { event: NDKEvent }) {
|
||||
export function NoteRepost() {
|
||||
const event = useNoteContext();
|
||||
const [isRepost, setIsRepost] = useState(false);
|
||||
|
||||
const submit = async () => {
|
||||
@@ -39,9 +40,9 @@ export function NoteRepost({ event }: { event: NDKEvent }) {
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content className="-left-10 inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-200 px-3.5 text-sm text-neutral-900 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-800 dark:text-neutral-100">
|
||||
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-white will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
||||
Repost
|
||||
<Tooltip.Arrow className="fill-neutral-200 dark:fill-neutral-800" />
|
||||
<Tooltip.Arrow className="fill-neutral-950" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
|
||||
@@ -16,8 +16,9 @@ import CurrencyInput from "react-currency-input-field";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useProfile } from "../../../hooks/useProfile";
|
||||
import { useArk, useStorage } from "../../../provider";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteZap({ event }: { event: NDKEvent }) {
|
||||
export function NoteZap() {
|
||||
const [walletConnectURL, setWalletConnectURL] = useState<string>(null);
|
||||
const [amount, setAmount] = useState<string>("21");
|
||||
const [zapMessage, setZapMessage] = useState<string>("");
|
||||
@@ -26,13 +27,14 @@ export function NoteZap({ event }: { event: NDKEvent }) {
|
||||
const [isCompleted, setIsCompleted] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { user } = useProfile(event.pubkey);
|
||||
|
||||
const ark = useArk();
|
||||
const event = useNoteContext();
|
||||
const storage = useStorage();
|
||||
const nwc = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { user } = useProfile(event.pubkey);
|
||||
|
||||
const createZapRequest = async () => {
|
||||
try {
|
||||
if (!ark.ndk.signer) return navigate("/new/privkey");
|
||||
|
||||
@@ -8,12 +8,14 @@ import { NoteArticleContent } from "./kinds/article";
|
||||
import { NoteMediaContent } from "./kinds/media";
|
||||
import { NoteTextContent } from "./kinds/text";
|
||||
import { NoteMenu } from "./menu";
|
||||
import { NoteProvider } from "./provider";
|
||||
import { NoteReplyList } from "./reply";
|
||||
import { NoteRoot } from "./root";
|
||||
import { NoteThread } from "./thread";
|
||||
import { NoteUser } from "./user";
|
||||
|
||||
export const Note = {
|
||||
Provider: NoteProvider,
|
||||
Root: NoteRoot,
|
||||
User: NoteUser,
|
||||
Menu: NoteMenu,
|
||||
@@ -30,6 +32,7 @@ export const Note = {
|
||||
ReplyList: NoteReplyList,
|
||||
};
|
||||
|
||||
export * from "./provider";
|
||||
export * from "./builds/text";
|
||||
export * from "./builds/repost";
|
||||
export * from "./builds/skeleton";
|
||||
|
||||
@@ -1,63 +1,76 @@
|
||||
import { HorizontalDotsIcon } from '@lume/icons';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { EventPointer } from 'nostr-tools/lib/types/nip19';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { HorizontalDotsIcon } from "@lume/icons";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { EventPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useNoteContext } from "./provider";
|
||||
|
||||
export function NoteMenu({ eventId, pubkey }: { eventId: string; pubkey: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
export function NoteMenu() {
|
||||
const event = useNoteContext();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const copyID = async () => {
|
||||
await writeText(nip19.neventEncode({ id: eventId, author: pubkey } as EventPointer));
|
||||
setOpen(false);
|
||||
};
|
||||
const copyID = async () => {
|
||||
await writeText(
|
||||
nip19.neventEncode({
|
||||
id: event.id,
|
||||
author: event.pubkey,
|
||||
} as EventPointer),
|
||||
);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const copyLink = async () => {
|
||||
await writeText(
|
||||
`https://njump.me/${nip19.neventEncode({ id: eventId, author: pubkey } as EventPointer)}`
|
||||
);
|
||||
setOpen(false);
|
||||
};
|
||||
const copyLink = async () => {
|
||||
await writeText(
|
||||
`https://njump.me/${nip19.neventEncode({
|
||||
id: event.id,
|
||||
author: event.pubkey,
|
||||
} as EventPointer)}`,
|
||||
);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button type="button" className="inline-flex h-6 w-6 items-center justify-center">
|
||||
<HorizontalDotsIcon className="h-4 w-4 text-neutral-800 hover:text-blue-500 dark:text-neutral-200" />
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-200 bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:bg-neutral-900">
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyLink()}
|
||||
className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-200 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||
>
|
||||
Copy shareable link
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyID()}
|
||||
className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-200 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||
>
|
||||
Copy ID
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<Link
|
||||
to={`/users/${pubkey}`}
|
||||
className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-200 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||
>
|
||||
View profile
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
return (
|
||||
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-6 w-6 items-center justify-center"
|
||||
>
|
||||
<HorizontalDotsIcon className="h-4 w-4 text-neutral-800 hover:text-blue-500 dark:text-neutral-200" />
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-200 bg-neutral-950 focus:outline-none dark:border-neutral-900">
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyLink()}
|
||||
className="inline-flex h-10 items-center px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none"
|
||||
>
|
||||
Copy shareable link
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyID()}
|
||||
className="inline-flex h-10 items-center px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none"
|
||||
>
|
||||
Copy note ID
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<Link
|
||||
to={`/users/${event.pubkey}`}
|
||||
className="inline-flex h-10 items-center px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none"
|
||||
>
|
||||
View profile
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
}
|
||||
|
||||
21
packages/ark/src/components/note/provider.tsx
Normal file
21
packages/ark/src/components/note/provider.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { ReactNode, createContext, useContext } from "react";
|
||||
|
||||
const EventContext = createContext<NDKEvent>(null);
|
||||
|
||||
export function NoteProvider({
|
||||
event,
|
||||
children,
|
||||
}: { event: NDKEvent; children: ReactNode }) {
|
||||
return (
|
||||
<EventContext.Provider value={event}>{children}</EventContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useNoteContext() {
|
||||
const context = useContext(EventContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("Please import Note Provider to use useNoteContext() hook");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from "@lume/utils";
|
||||
import { ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function NoteRoot({
|
||||
children,
|
||||
@@ -10,7 +10,7 @@ export function NoteRoot({
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
className={cn(
|
||||
"flex h-min w-full flex-col overflow-hidden rounded-xl bg-neutral-50 dark:bg-neutral-950",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function NoteThread({
|
||||
to={`/events/${thread?.rootEventId || thread?.replyEventId}`}
|
||||
className="self-start text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
Show full thread
|
||||
Show thread
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,29 +5,33 @@ import { minidenticon } from "minidenticons";
|
||||
import { useMemo } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import { useNoteContext } from "./provider";
|
||||
|
||||
export function NoteUser({
|
||||
pubkey,
|
||||
time,
|
||||
variant = "text",
|
||||
className,
|
||||
}: {
|
||||
pubkey: string;
|
||||
time: number;
|
||||
variant?: "text" | "repost" | "mention" | "thread";
|
||||
className?: string;
|
||||
}) {
|
||||
const createdAt = useMemo(() => formatCreatedAt(time), [time]);
|
||||
const fallbackName = useMemo(() => displayNpub(pubkey, 16), [pubkey]);
|
||||
const event = useNoteContext();
|
||||
const createdAt = useMemo(
|
||||
() => formatCreatedAt(event.created_at),
|
||||
[event.created_at],
|
||||
);
|
||||
const fallbackName = useMemo(
|
||||
() => displayNpub(event.pubkey, 16),
|
||||
[event.pubkey],
|
||||
);
|
||||
const fallbackAvatar = useMemo(
|
||||
() =>
|
||||
`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(pubkey, 90, 50),
|
||||
minidenticon(event.pubkey, 90, 50),
|
||||
)}`,
|
||||
[pubkey],
|
||||
[event.pubkey],
|
||||
);
|
||||
|
||||
const { isLoading, user } = useProfile(pubkey);
|
||||
const { isLoading, user } = useProfile(event.pubkey);
|
||||
|
||||
if (variant === "mention") {
|
||||
if (isLoading) {
|
||||
@@ -36,7 +40,7 @@ export function NoteUser({
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
alt={event.pubkey}
|
||||
className="h-6 w-6 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Root>
|
||||
@@ -58,7 +62,7 @@ export function NoteUser({
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
alt={event.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-6 w-6 rounded-md"
|
||||
@@ -66,7 +70,7 @@ export function NoteUser({
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
alt={event.pubkey}
|
||||
className="h-6 w-6 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
@@ -111,7 +115,7 @@ export function NoteUser({
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
alt={event.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-6 w-6 rounded object-cover"
|
||||
@@ -119,7 +123,7 @@ export function NoteUser({
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
alt={event.pubkey}
|
||||
className="h-6 w-6 rounded bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
@@ -156,7 +160,7 @@ export function NoteUser({
|
||||
<Avatar.Root className="h-10 w-10 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
alt={event.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-10 w-10 rounded-lg object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
|
||||
@@ -164,7 +168,7 @@ export function NoteUser({
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
alt={event.pubkey}
|
||||
className="h-10 w-10 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
@@ -189,7 +193,7 @@ export function NoteUser({
|
||||
<Avatar.Root className="h-9 w-9 shrink-0">
|
||||
<Avatar.Image
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
alt={event.pubkey}
|
||||
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
||||
/>
|
||||
</Avatar.Root>
|
||||
@@ -207,7 +211,7 @@ export function NoteUser({
|
||||
<Avatar.Root className="h-9 w-9 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
alt={event.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-9 w-9 rounded-lg bg-white object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
|
||||
@@ -215,7 +219,7 @@ export function NoteUser({
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
alt={event.pubkey}
|
||||
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>
|
||||
|
||||
@@ -1,183 +1,209 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { ReactNode } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import {
|
||||
Hashtag,
|
||||
ImagePreview,
|
||||
LinkPreview,
|
||||
MentionNote,
|
||||
MentionUser,
|
||||
VideoPreview,
|
||||
} from '../components/note';
|
||||
import { useStorage } from '../provider';
|
||||
Hashtag,
|
||||
ImagePreview,
|
||||
LinkPreview,
|
||||
MentionNote,
|
||||
MentionUser,
|
||||
VideoPreview,
|
||||
} from "../components/note";
|
||||
import { useStorage } from "../provider";
|
||||
|
||||
const NOSTR_MENTIONS = [
|
||||
'@npub1',
|
||||
'nostr:npub1',
|
||||
'nostr:nprofile1',
|
||||
'nostr:naddr1',
|
||||
'npub1',
|
||||
'nprofile1',
|
||||
'naddr1',
|
||||
'Nostr:npub1',
|
||||
'Nostr:nprofile1',
|
||||
'Nostr:naddre1',
|
||||
"@npub1",
|
||||
"nostr:npub1",
|
||||
"nostr:nprofile1",
|
||||
"nostr:naddr1",
|
||||
"npub1",
|
||||
"nprofile1",
|
||||
"naddr1",
|
||||
"Nostr:npub1",
|
||||
"Nostr:nprofile1",
|
||||
"Nostr:naddre1",
|
||||
];
|
||||
|
||||
const NOSTR_EVENTS = [
|
||||
'nostr:note1',
|
||||
'note1',
|
||||
'nostr:nevent1',
|
||||
'nevent1',
|
||||
'Nostr:note1',
|
||||
'Nostr:nevent1',
|
||||
"nostr:note1",
|
||||
"note1",
|
||||
"nostr:nevent1",
|
||||
"nevent1",
|
||||
"Nostr:note1",
|
||||
"Nostr:nevent1",
|
||||
];
|
||||
|
||||
// const BITCOINS = ['lnbc', 'bc1p', 'bc1q'];
|
||||
|
||||
const IMAGES = ['.jpg', '.jpeg', '.gif', '.png', '.webp', '.avif', '.tiff'];
|
||||
const IMAGES = [".jpg", ".jpeg", ".gif", ".png", ".webp", ".avif", ".tiff"];
|
||||
|
||||
const VIDEOS = [
|
||||
'.mp4',
|
||||
'.mov',
|
||||
'.webm',
|
||||
'.wmv',
|
||||
'.flv',
|
||||
'.mts',
|
||||
'.avi',
|
||||
'.ogv',
|
||||
'.mkv',
|
||||
'.mp3',
|
||||
'.m3u8',
|
||||
".mp4",
|
||||
".mov",
|
||||
".webm",
|
||||
".wmv",
|
||||
".flv",
|
||||
".mts",
|
||||
".avi",
|
||||
".ogv",
|
||||
".mkv",
|
||||
".mp3",
|
||||
".m3u8",
|
||||
];
|
||||
|
||||
export function useRichContent(content: string, textmode = false) {
|
||||
const storage = useStorage();
|
||||
const storage = useStorage();
|
||||
|
||||
let parsedContent: string | ReactNode[] = content.replace(/\n+/g, '\n');
|
||||
let linkPreview: string;
|
||||
let images: string[] = [];
|
||||
let videos: string[] = [];
|
||||
let events: string[] = [];
|
||||
let parsedContent: string | ReactNode[] = content.replace(/\n+/g, "\n");
|
||||
let linkPreview: string;
|
||||
let images: string[] = [];
|
||||
let videos: string[] = [];
|
||||
let events: string[] = [];
|
||||
|
||||
const text = parsedContent;
|
||||
const words = text.split(/( |\n)/);
|
||||
const text = parsedContent;
|
||||
const words = text.split(/( |\n)/);
|
||||
|
||||
if (!textmode) {
|
||||
if (storage.settings.media) {
|
||||
images = words.filter((word) => IMAGES.some((el) => word.endsWith(el)));
|
||||
videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el)));
|
||||
}
|
||||
events = words.filter((word) => NOSTR_EVENTS.some((el) => word.startsWith(el)));
|
||||
}
|
||||
if (!textmode) {
|
||||
if (storage.settings.media) {
|
||||
images = words.filter((word) => IMAGES.some((el) => word.endsWith(el)));
|
||||
videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el)));
|
||||
}
|
||||
events = words.filter((word) =>
|
||||
NOSTR_EVENTS.some((el) => word.startsWith(el)),
|
||||
);
|
||||
}
|
||||
|
||||
const hashtags = words.filter((word) => word.startsWith('#'));
|
||||
const mentions = words.filter((word) =>
|
||||
NOSTR_MENTIONS.some((el) => word.startsWith(el))
|
||||
);
|
||||
const hashtags = words.filter((word) => word.startsWith("#"));
|
||||
const mentions = words.filter((word) =>
|
||||
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
|
||||
);
|
||||
|
||||
try {
|
||||
if (images.length) {
|
||||
for(const image of images) {
|
||||
parsedContent = reactStringReplace(parsedContent, image, (match, i) => (
|
||||
<ImagePreview key={match + i} url={match} />
|
||||
));
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (images.length) {
|
||||
for (const image of images) {
|
||||
parsedContent = reactStringReplace(parsedContent, image, (match, i) => (
|
||||
<ImagePreview key={match + i} url={match} />
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (videos.length) {
|
||||
for(const video of videos) {
|
||||
parsedContent = reactStringReplace(parsedContent, video, (match, i) => (
|
||||
<VideoPreview key={match + i} url={match} />
|
||||
));
|
||||
}
|
||||
}
|
||||
if (videos.length) {
|
||||
for (const video of videos) {
|
||||
parsedContent = reactStringReplace(parsedContent, video, (match, i) => (
|
||||
<VideoPreview key={match + i} url={match} />
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (hashtags.length) {
|
||||
for(const hashtag of hashtags) {
|
||||
parsedContent = reactStringReplace(parsedContent, hashtag, (match, i) => {
|
||||
if (storage.settings.hashtag) return <Hashtag key={match + i} tag={hashtag} />;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (hashtags.length) {
|
||||
for (const hashtag of hashtags) {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
hashtag,
|
||||
(match, i) => {
|
||||
if (storage.settings.hashtag)
|
||||
return <Hashtag key={match + i} tag={hashtag} />;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (events.length) {
|
||||
for(const event of events) {
|
||||
const address = event.replace('nostr:', '').replace(/[^a-zA-Z0-9]/g, '');
|
||||
const decoded = nip19.decode(address);
|
||||
if (events.length) {
|
||||
for (const event of events) {
|
||||
const address = event
|
||||
.replace("nostr:", "")
|
||||
.replace(/[^a-zA-Z0-9]/g, "");
|
||||
const decoded = nip19.decode(address);
|
||||
|
||||
if (decoded.type === 'note') {
|
||||
parsedContent = reactStringReplace(parsedContent, event, (match, i) => (
|
||||
<MentionNote key={match + i} eventId={decoded.data} />
|
||||
));
|
||||
}
|
||||
if (decoded.type === "note") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
event,
|
||||
(match, i) => (
|
||||
<MentionNote key={match + i} eventId={decoded.data} />
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (decoded.type === 'nevent') {
|
||||
parsedContent = reactStringReplace(parsedContent, event, (match, i) => (
|
||||
<MentionNote key={match + i} eventId={decoded.data.id} />
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (decoded.type === "nevent") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
event,
|
||||
(match, i) => (
|
||||
<MentionNote key={match + i} eventId={decoded.data.id} />
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mentions.length) {
|
||||
for(const mention of mentions) {
|
||||
const address = mention
|
||||
.replace('nostr:', '')
|
||||
.replace('@', '')
|
||||
.replace(/[^a-zA-Z0-9]/g, '');
|
||||
const decoded = nip19.decode(address);
|
||||
if (mentions.length) {
|
||||
for (const mention of mentions) {
|
||||
const address = mention
|
||||
.replace("nostr:", "")
|
||||
.replace("@", "")
|
||||
.replace(/[^a-zA-Z0-9]/g, "");
|
||||
const decoded = nip19.decode(address);
|
||||
|
||||
if (decoded.type === 'npub') {
|
||||
parsedContent = reactStringReplace(parsedContent, mention, (match, i) => (
|
||||
<MentionUser key={match + i} pubkey={decoded.data} />
|
||||
));
|
||||
}
|
||||
if (decoded.type === "npub") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
mention,
|
||||
(match, i) => <MentionUser key={match + i} pubkey={decoded.data} />,
|
||||
);
|
||||
}
|
||||
|
||||
if (decoded.type === 'nprofile' || decoded.type === 'naddr') {
|
||||
parsedContent = reactStringReplace(parsedContent, mention, (match, i) => (
|
||||
<MentionUser key={match + i} pubkey={decoded.data.pubkey} />
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (decoded.type === "nprofile" || decoded.type === "naddr") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
mention,
|
||||
(match, i) => (
|
||||
<MentionUser key={match + i} pubkey={decoded.data.pubkey} />
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => {
|
||||
const url = new URL(match);
|
||||
url.search = '';
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
/(https?:\/\/\S+)/g,
|
||||
(match, i) => {
|
||||
const url = new URL(match);
|
||||
url.search = "";
|
||||
|
||||
if (!linkPreview && !textmode) {
|
||||
linkPreview = match;
|
||||
return <LinkPreview key={match + i} url={url.toString()} />;
|
||||
}
|
||||
if (!linkPreview && !textmode) {
|
||||
linkPreview = match;
|
||||
return <LinkPreview key={match + i} url={url.toString()} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={match + i}
|
||||
to={url.toString()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="break-all font-normal text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url.toString()}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Link
|
||||
key={match + i}
|
||||
to={url.toString()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="break-all font-normal text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url.toString()}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
parsedContent = reactStringReplace(parsedContent, '\n', () => {
|
||||
return <div key={nanoid()} className="h-3" />;
|
||||
});
|
||||
parsedContent = reactStringReplace(parsedContent, "\n", () => {
|
||||
return null;
|
||||
});
|
||||
|
||||
if (typeof parsedContent[0] === 'string') {
|
||||
parsedContent[0] = parsedContent[0].trimStart();
|
||||
}
|
||||
if (typeof parsedContent[0] === "string") {
|
||||
parsedContent[0] = parsedContent[0].trimStart();
|
||||
}
|
||||
|
||||
return { parsedContent };
|
||||
} catch (e) {
|
||||
console.warn('[parser] parse failed: ', e);
|
||||
return { parsedContent };
|
||||
}
|
||||
return { parsedContent };
|
||||
} catch (e) {
|
||||
console.warn("[parser] parse failed: ", e);
|
||||
return { parsedContent };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user