refactor(ark): add note provider

This commit is contained in:
2023-12-27 10:52:13 +07:00
parent 3956ed622d
commit b4dac2d477
38 changed files with 793 additions and 1140 deletions

View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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");

View File

@@ -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";

View File

@@ -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>
);
}

View 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;
}

View File

@@ -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,
)}

View File

@@ -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>

View File

@@ -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>