refactor note component
This commit is contained in:
29
src/shared/notes/actions.tsx
Normal file
29
src/shared/notes/actions.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
|
||||
import { NoteReaction } from '@shared/notes/actions/reaction';
|
||||
import { NoteReply } from '@shared/notes/actions/reply';
|
||||
import { NoteRepost } from '@shared/notes/actions/repost';
|
||||
import { NoteZap } from '@shared/notes/actions/zap';
|
||||
|
||||
export function NoteActions({
|
||||
id,
|
||||
rootID,
|
||||
eventPubkey,
|
||||
}: {
|
||||
id: string;
|
||||
rootID?: string;
|
||||
eventPubkey: string;
|
||||
}) {
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
<div className="-ml-1 mt-4 inline-flex w-full items-center justify-between">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<NoteReaction />
|
||||
<NoteZap />
|
||||
<NoteRepost id={id} pubkey={eventPubkey} />
|
||||
<NoteReply id={id} rootID={rootID} pubkey={eventPubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
32
src/shared/notes/actions/reaction.tsx
Normal file
32
src/shared/notes/actions/reaction.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
|
||||
import { ReactionIcon } from '@shared/icons';
|
||||
|
||||
export function NoteReaction() {
|
||||
const submit = async () => {
|
||||
console.log('todo');
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="group inline-flex h-7 w-7 items-center justify-center"
|
||||
>
|
||||
<ReactionIcon className="h-5 w-5 text-zinc-300 group-hover:text-red-400" />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="-left-10 select-none rounded-md bg-zinc-800/80 px-3.5 py-1.5 text-sm leading-none text-zinc-100 backdrop-blur-lg 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"
|
||||
sideOffset={5}
|
||||
>
|
||||
Reaction
|
||||
<Tooltip.Arrow className="fill-zinc-800/80 backdrop-blur-lg" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
}
|
||||
@@ -4,37 +4,28 @@ import { ReplyIcon } from '@shared/icons';
|
||||
|
||||
import { useComposer } from '@stores/composer';
|
||||
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function NoteReply({
|
||||
id,
|
||||
rootID,
|
||||
pubkey,
|
||||
replies,
|
||||
}: {
|
||||
id: string;
|
||||
rootID?: string;
|
||||
pubkey: string;
|
||||
replies: number;
|
||||
}) {
|
||||
const setReply = useComposer((state) => state.setReply);
|
||||
|
||||
return (
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReply(id, rootID, pubkey)}
|
||||
className="group group inline-flex h-6 w-20 items-center gap-1.5"
|
||||
>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded group-hover:bg-zinc-800">
|
||||
<ReplyIcon className="h-4 w-4 text-zinc-400 group-hover:text-green-500" />
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-zinc-100">
|
||||
{compactNumber.format(replies)}
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReply(id, rootID, pubkey)}
|
||||
className="group inline-flex h-7 w-7 items-center justify-center"
|
||||
>
|
||||
<ReplyIcon className="h-5 w-5 text-zinc-300 group-hover:text-green-500" />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="-left-10 select-none rounded-md bg-zinc-800/80 px-3.5 py-1.5 text-sm leading-none text-zinc-100 backdrop-blur-lg 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"
|
||||
@@ -5,17 +5,8 @@ import { RepostIcon } from '@shared/icons';
|
||||
import { FULL_RELAYS } from '@stores/constants';
|
||||
|
||||
import { usePublish } from '@utils/hooks/usePublish';
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function NoteRepost({
|
||||
id,
|
||||
pubkey,
|
||||
reposts,
|
||||
}: {
|
||||
id: string;
|
||||
pubkey: string;
|
||||
reposts: number;
|
||||
}) {
|
||||
export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
|
||||
const publish = usePublish();
|
||||
|
||||
const submit = async () => {
|
||||
@@ -28,20 +19,15 @@ export function NoteRepost({
|
||||
|
||||
return (
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="group group inline-flex h-6 w-20 items-center gap-1.5"
|
||||
>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded group-hover:bg-zinc-800">
|
||||
<RepostIcon className="h-4 w-4 text-zinc-400 group-hover:text-blue-400" />
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-zinc-100">
|
||||
{compactNumber.format(reposts)}
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="group inline-flex h-7 w-7 items-center justify-center"
|
||||
>
|
||||
<RepostIcon className="h-5 w-5 text-zinc-300 group-hover:text-blue-400" />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="-left-10 select-none rounded-md bg-zinc-800/80 px-3.5 py-1.5 text-sm leading-none text-zinc-100 backdrop-blur-lg 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"
|
||||
@@ -2,30 +2,23 @@ import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
|
||||
import { ZapIcon } from '@shared/icons';
|
||||
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function NoteZap({ zaps }: { zaps: number }) {
|
||||
export function NoteZap() {
|
||||
return (
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<button
|
||||
type="button"
|
||||
className="group group inline-flex h-6 w-20 items-center gap-1.5"
|
||||
>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded group-hover:bg-zinc-800">
|
||||
<ZapIcon className="h-4 w-4 text-zinc-400 group-hover:text-orange-400" />
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-zinc-100">
|
||||
{compactNumber.format(zaps)}
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="group inline-flex h-7 w-7 items-center justify-center"
|
||||
>
|
||||
<ZapIcon className="h-5 w-5 text-zinc-300 group-hover:text-orange-400" />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="-left-10 select-none rounded-md bg-zinc-800/80 px-3.5 py-1.5 text-sm leading-none text-zinc-100 backdrop-blur-lg 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"
|
||||
sideOffset={5}
|
||||
>
|
||||
Coming Soon
|
||||
Tip
|
||||
<Tooltip.Arrow className="fill-zinc-800/80 backdrop-blur-lg" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
@@ -1,40 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { MentionNote } from '@shared/notes/mentions/note';
|
||||
import { ImagePreview } from '@shared/notes/preview/image';
|
||||
import { LinkPreview } from '@shared/notes/preview/link';
|
||||
import { VideoPreview } from '@shared/notes/preview/video';
|
||||
|
||||
export function Kind1({
|
||||
content,
|
||||
truncate = false,
|
||||
}: {
|
||||
content: {
|
||||
original: string;
|
||||
parsed: ReactNode[];
|
||||
notes: string[];
|
||||
images: string[];
|
||||
videos: string[];
|
||||
links: string[];
|
||||
};
|
||||
truncate?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`select-text whitespace-pre-line break-words text-base text-zinc-100 ${
|
||||
truncate ? 'line-clamp-3' : ''
|
||||
}`}
|
||||
>
|
||||
{content.parsed}
|
||||
</div>
|
||||
{content.images.length > 0 && (
|
||||
<ImagePreview urls={content.images} truncate={truncate} />
|
||||
)}
|
||||
{content.videos.length > 0 && <VideoPreview urls={content.videos} />}
|
||||
{content.links.length > 0 && <LinkPreview urls={content.links} />}
|
||||
{content.notes.length > 0 &&
|
||||
content.notes.map((note: string) => <MentionNote key={note} id={note} />)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { NDKTag } from '@nostr-dev-kit/ndk';
|
||||
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
function isImage(url: string) {
|
||||
return /\.(jpg|jpeg|gif|png|webp|avif)$/.test(url);
|
||||
}
|
||||
|
||||
export function Kind1063({ metadata }: { metadata: NDKTag[] }) {
|
||||
const url = metadata[0][1];
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
{isImage(url) && (
|
||||
<Image
|
||||
src={url}
|
||||
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
||||
alt="image"
|
||||
className="h-auto w-full rounded-lg object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/shared/notes/index.tsx
Normal file
22
src/shared/notes/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
export * from './actions/reaction';
|
||||
export * from './actions/reply';
|
||||
export * from './actions/repost';
|
||||
export * from './actions/zap';
|
||||
export * from './mentions/note';
|
||||
export * from './mentions/user';
|
||||
export * from './preview/image';
|
||||
export * from './preview/link';
|
||||
export * from './preview/video';
|
||||
export * from './replies/form';
|
||||
export * from './replies/item';
|
||||
export * from './replies/list';
|
||||
export * from './kinds/kind1';
|
||||
export * from './kinds/kind1063';
|
||||
export * from './metadata';
|
||||
export * from './users/mini';
|
||||
export * from './users/repost';
|
||||
export * from './kinds/thread';
|
||||
export * from './kinds/repost';
|
||||
export * from './kinds/sub';
|
||||
export * from './skeleton';
|
||||
export * from './actions';
|
||||
52
src/shared/notes/kinds/kind1.tsx
Normal file
52
src/shared/notes/kinds/kind1.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { LinkPreview, NoteActions, NoteMetadata, VideoPreview } from '@shared/notes';
|
||||
import { MentionNote } from '@shared/notes/mentions/note';
|
||||
import { ImagePreview } from '@shared/notes/preview/image';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { parser } from '@utils/parser';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function NoteKind_1({
|
||||
event,
|
||||
skipMetadata = false,
|
||||
}: {
|
||||
event: LumeEvent;
|
||||
skipMetadata?: boolean;
|
||||
}) {
|
||||
const content = useMemo(() => parser(event), [event.id]);
|
||||
|
||||
return (
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="relative overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||
<div className="relative flex flex-col">
|
||||
<User pubkey={event.pubkey} time={event.created_at} />
|
||||
<div className="relative z-20 -mt-5 flex items-start gap-3">
|
||||
<div className="w-11 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="relative z-10 select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
{content.parsed}
|
||||
</div>
|
||||
{content.images.length > 0 && <ImagePreview urls={content.images} />}
|
||||
{content.videos.length > 0 && <VideoPreview urls={content.videos} />}
|
||||
{content.links.length > 0 && <LinkPreview urls={content.links} />}
|
||||
{content.notes.length > 0 &&
|
||||
content.notes.map((note: string) => <MentionNote key={note} id={note} />)}
|
||||
<NoteActions
|
||||
id={event.event_id}
|
||||
rootID={event.parent_id}
|
||||
eventPubkey={event.pubkey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!skipMetadata ? (
|
||||
<NoteMetadata id={event.event_id} />
|
||||
) : (
|
||||
<div className="pb-3" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
src/shared/notes/kinds/kind1063.tsx
Normal file
42
src/shared/notes/kinds/kind1063.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Image } from '@shared/image';
|
||||
import { NoteActions, NoteMetadata } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
function isImage(url: string) {
|
||||
return /\.(jpg|jpeg|gif|png|webp|avif)$/.test(url);
|
||||
}
|
||||
|
||||
export function NoteKind_1063({ event }: { event: LumeEvent }) {
|
||||
const url = event.tags[0][1];
|
||||
|
||||
return (
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="relative overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||
<div className="flex flex-col">
|
||||
<User pubkey={event.pubkey} time={event.created_at} />
|
||||
<div className="relative z-20 -mt-5 flex items-start gap-3">
|
||||
<div className="w-11 shrink-0" />
|
||||
<div className="flex-1">
|
||||
{isImage(url) && (
|
||||
<Image
|
||||
src={url}
|
||||
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
||||
alt="image"
|
||||
className="h-auto w-full rounded-lg object-cover"
|
||||
/>
|
||||
)}
|
||||
<NoteActions
|
||||
id={event.event_id}
|
||||
rootID={event.parent_id}
|
||||
eventPubkey={event.pubkey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NoteMetadata id={event.event_id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
src/shared/notes/kinds/repost.tsx
Normal file
74
src/shared/notes/kinds/repost.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
ImagePreview,
|
||||
LinkPreview,
|
||||
MentionNote,
|
||||
NoteActions,
|
||||
NoteMetadata,
|
||||
NoteSkeleton,
|
||||
RepostUser,
|
||||
VideoPreview,
|
||||
} from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useEvent } from '@utils/hooks/useEvent';
|
||||
import { getRepostID } from '@utils/transform';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function Repost({ event }: { event: LumeEvent }) {
|
||||
const repostID = getRepostID(event.tags);
|
||||
const { status, data } = useEvent(repostID, event.content);
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="relative overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<div className="flex items-center justify-center overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
|
||||
<p className="text-zinc-400">Failed to fetch</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="relative overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||
<div className="flex flex-col">
|
||||
<div className="isolate flex flex-col -space-y-4 overflow-hidden">
|
||||
<RepostUser pubkey={event.pubkey} />
|
||||
<User pubkey={data.pubkey} time={event.created_at} isRepost={true} />
|
||||
</div>
|
||||
<div className="relative z-20 flex items-start gap-3">
|
||||
<div className="w-11 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="relative z-10 select-text whitespace-pre-line break-all text-base text-zinc-100">
|
||||
{data.content.parsed}
|
||||
</div>
|
||||
{data.content.images.length > 0 && (
|
||||
<ImagePreview urls={data.content.images} />
|
||||
)}
|
||||
{data.content.videos.length > 0 && (
|
||||
<VideoPreview urls={data.content.videos} />
|
||||
)}
|
||||
{data.content.links.length > 0 && <LinkPreview urls={data.content.links} />}
|
||||
{data.content.notes.length > 0 &&
|
||||
data.content.notes.map((note: string) => (
|
||||
<MentionNote key={note} id={note} />
|
||||
))}
|
||||
<NoteActions
|
||||
id={event.event_id}
|
||||
rootID={event.parent_id}
|
||||
eventPubkey={event.pubkey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NoteMetadata id={event.event_id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
src/shared/notes/kinds/sub.tsx
Normal file
60
src/shared/notes/kinds/sub.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
ImagePreview,
|
||||
LinkPreview,
|
||||
MentionNote,
|
||||
NoteActions,
|
||||
NoteSkeleton,
|
||||
VideoPreview,
|
||||
} from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useEvent } from '@utils/hooks/useEvent';
|
||||
|
||||
export function SubNote({ id }: { id: string }) {
|
||||
const { status, data } = useEvent(id);
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="relative overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<div className="flex items-center justify-center overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
|
||||
<p className="text-zinc-400">Failed to fetch</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
|
||||
<div className="mb-5 flex flex-col">
|
||||
<User pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="relative z-20 -mt-5 flex items-start gap-3">
|
||||
<div className="w-11 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="relative z-10 select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
{data.content.parsed}
|
||||
</div>
|
||||
{data.content.images.length > 0 && (
|
||||
<ImagePreview urls={data.content.images} />
|
||||
)}
|
||||
{data.content.videos.length > 0 && (
|
||||
<VideoPreview urls={data.content.videos} />
|
||||
)}
|
||||
{data.content.links.length > 0 && <LinkPreview urls={data.content.links} />}
|
||||
{data.content.notes.length > 0 &&
|
||||
data.content.notes.map((note: string) => (
|
||||
<MentionNote key={note} id={note} />
|
||||
))}
|
||||
<NoteActions id={data.id} eventPubkey={data.pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
58
src/shared/notes/kinds/thread.tsx
Normal file
58
src/shared/notes/kinds/thread.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
LinkPreview,
|
||||
NoteActions,
|
||||
NoteMetadata,
|
||||
SubNote,
|
||||
VideoPreview,
|
||||
} from '@shared/notes';
|
||||
import { MentionNote } from '@shared/notes/mentions/note';
|
||||
import { ImagePreview } from '@shared/notes/preview/image';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { parser } from '@utils/parser';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function NoteThread({
|
||||
event,
|
||||
root,
|
||||
reply,
|
||||
}: {
|
||||
event: LumeEvent;
|
||||
root: string;
|
||||
reply: string;
|
||||
}) {
|
||||
const content = useMemo(() => parser(event), [event.id]);
|
||||
|
||||
return (
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||
<div className="relative">{root && <SubNote id={root} />}</div>
|
||||
<div className="relative">{reply && <SubNote id={reply} />}</div>
|
||||
<div className="relative flex flex-col">
|
||||
<User pubkey={event.pubkey} time={event.created_at} />
|
||||
<div className="relative z-20 -mt-5 flex items-start gap-3">
|
||||
<div className="w-11 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="relative z-10 select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
{content.parsed}
|
||||
</div>
|
||||
{content.images.length > 0 && <ImagePreview urls={content.images} />}
|
||||
{content.videos.length > 0 && <VideoPreview urls={content.videos} />}
|
||||
{content.links.length > 0 && <LinkPreview urls={content.links} />}
|
||||
{content.notes.length > 0 &&
|
||||
content.notes.map((note: string) => <MentionNote key={note} id={note} />)}
|
||||
<NoteActions
|
||||
id={event.event_id}
|
||||
rootID={event.parent_id}
|
||||
eventPubkey={event.pubkey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NoteMetadata id={event.event_id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
src/shared/notes/kinds/unsupport.tsx
Normal file
40
src/shared/notes/kinds/unsupport.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NoteActions, NoteMetadata } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function NoteKindUnsupport({ event }: { event: LumeEvent }) {
|
||||
return (
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="relative overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||
<div className="flex flex-col">
|
||||
<User pubkey={event.pubkey} time={event.created_at} />
|
||||
<div className="relative z-20 -mt-6 flex items-start gap-3">
|
||||
<div className="w-11 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="mt-3 flex w-full flex-col gap-2">
|
||||
<div className="inline-flex flex-col gap-1 rounded-md bg-zinc-800 px-2 py-2">
|
||||
<span className="text-sm font-medium leading-none text-zinc-500">
|
||||
Kind: {event.kind}
|
||||
</span>
|
||||
<p className="text-sm leading-none text-fuchsia-500">
|
||||
Lume isn't fully support this kind
|
||||
</p>
|
||||
</div>
|
||||
<div className="select-text whitespace-pre-line break-all text-zinc-100">
|
||||
<p>{event.content.toString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<NoteActions
|
||||
id={event.event_id}
|
||||
rootID={event.parent_id}
|
||||
eventPubkey={event.pubkey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NoteMetadata id={event.event_id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,17 +3,15 @@ import { memo } from 'react';
|
||||
|
||||
import { createBlock } from '@libs/storage';
|
||||
|
||||
import { Kind1 } from '@shared/notes/contents/kind1';
|
||||
import { Kind1063 } from '@shared/notes/contents/kind1063';
|
||||
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useEvent } from '@utils/hooks/useEvent';
|
||||
|
||||
export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
const { status, data } = useEvent(id);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const block = useMutation({
|
||||
mutationFn: (data: any) => {
|
||||
return createBlock(data.kind, data.title, data.content);
|
||||
@@ -45,25 +43,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||
) : status === 'success' ? (
|
||||
<>
|
||||
<User pubkey={data.pubkey} time={data.created_at} size="small" />
|
||||
<div>
|
||||
{data.kind === 1 && <Kind1 content={data.content} truncate={true} />}
|
||||
{data.kind === 1063 && <Kind1063 metadata={data.tags} />}
|
||||
{data.kind !== 1 && data.kind !== 1063 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="inline-flex flex-col gap-1 rounded-md bg-zinc-800 px-2 py-2">
|
||||
<span className="text-sm font-medium leading-none text-zinc-500">
|
||||
Kind: {data.kind}
|
||||
</span>
|
||||
<p className="text-sm leading-none text-fuchsia-500">
|
||||
Lume isn't fully support this kind in newsfeed
|
||||
</p>
|
||||
</div>
|
||||
<div className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
<p>{data.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex items-start gap-3">{data.content.parsed}</div>
|
||||
</>
|
||||
) : (
|
||||
<p>Failed to fetch event</p>
|
||||
|
||||
@@ -1,74 +1,66 @@
|
||||
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { decode } from 'light-bolt11-decoder';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { createBlock, createReplyNote } from '@libs/storage';
|
||||
|
||||
import { LoaderIcon, ReplyIcon, RepostIcon, ZapIcon } from '@shared/icons';
|
||||
import { ThreadIcon } from '@shared/icons/thread';
|
||||
import { NoteReply } from '@shared/notes/metadata/reply';
|
||||
import { NoteRepost } from '@shared/notes/metadata/repost';
|
||||
import { NoteZap } from '@shared/notes/metadata/zap';
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { MiniUser } from '@shared/notes/users/mini';
|
||||
|
||||
export function NoteMetadata({
|
||||
id,
|
||||
rootID,
|
||||
eventPubkey,
|
||||
}: {
|
||||
id: string;
|
||||
rootID?: string;
|
||||
eventPubkey: string;
|
||||
}) {
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function NoteMetadata({ id }: { id: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(['note-metadata', id], async () => {
|
||||
let replies = 0;
|
||||
let reposts = 0;
|
||||
let zap = 0;
|
||||
const { status, data } = useQuery(
|
||||
['note-metadata', id],
|
||||
async () => {
|
||||
let replies = 0;
|
||||
let zap = 0;
|
||||
const users = [];
|
||||
|
||||
const filter: NDKFilter = {
|
||||
'#e': [id],
|
||||
kinds: [1, 6, 9735],
|
||||
};
|
||||
const filter: NDKFilter = {
|
||||
'#e': [id],
|
||||
kinds: [1, 9735],
|
||||
};
|
||||
|
||||
const events = await ndk.fetchEvents(filter);
|
||||
events.forEach((event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case 1:
|
||||
replies += 1;
|
||||
createReplyNote(
|
||||
id,
|
||||
event.id,
|
||||
event.pubkey,
|
||||
event.kind,
|
||||
event.tags,
|
||||
event.content,
|
||||
event.created_at
|
||||
);
|
||||
break;
|
||||
case 6:
|
||||
reposts += 1;
|
||||
break;
|
||||
case 9735: {
|
||||
const bolt11 = event.tags.find((tag) => tag[0] === 'bolt11')[1];
|
||||
if (bolt11) {
|
||||
const decoded = decode(bolt11);
|
||||
const amount = decoded.sections.find((item) => item.name === 'amount');
|
||||
const sats = amount.value / 1000;
|
||||
zap += sats;
|
||||
const events = await ndk.fetchEvents(filter);
|
||||
events.forEach((event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case 1:
|
||||
replies += 1;
|
||||
if (users.length < 3) users.push(event.pubkey);
|
||||
createReplyNote(
|
||||
id,
|
||||
event.id,
|
||||
event.pubkey,
|
||||
event.kind,
|
||||
event.tags,
|
||||
event.content,
|
||||
event.created_at
|
||||
);
|
||||
break;
|
||||
case 9735: {
|
||||
const bolt11 = event.tags.find((tag) => tag[0] === 'bolt11')[1];
|
||||
if (bolt11) {
|
||||
const decoded = decode(bolt11);
|
||||
const amount = decoded.sections.find((item) => item.name === 'amount');
|
||||
const sats = amount.value / 1000;
|
||||
zap += sats;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { replies, reposts, zap };
|
||||
});
|
||||
return { replies, users, zap };
|
||||
},
|
||||
{ refetchOnWindowFocus: false, refetchOnReconnect: false }
|
||||
);
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: (data: any) => {
|
||||
@@ -85,81 +77,50 @@ export function NoteMetadata({
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="mt-2 inline-flex h-12 w-full items-center">
|
||||
<div className="group inline-flex w-20 items-center gap-1.5">
|
||||
<ReplyIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="group inline-flex w-20 items-center gap-1.5">
|
||||
<RepostIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="group inline-flex w-20 items-center gap-1.5">
|
||||
<ZapIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-zinc-100"
|
||||
/>
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<div className="mt-2h-6 w-11 shrink-0"></div>
|
||||
<div className="mt-2 inline-flex h-6 items-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-zinc-100" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
<div className="mt-2 inline-flex h-12 w-full items-center justify-between">
|
||||
<div className="inline-flex items-center justify-between">
|
||||
<NoteReply
|
||||
id={id}
|
||||
rootID={rootID}
|
||||
pubkey={eventPubkey}
|
||||
replies={data.replies}
|
||||
/>
|
||||
<NoteRepost id={id} pubkey={eventPubkey} reposts={data.reposts} />
|
||||
<NoteZap zaps={data.zap} />
|
||||
</div>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openThread(id)}
|
||||
className="inline-flex h-6 w-6 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800 hover:bg-zinc-700"
|
||||
>
|
||||
<ThreadIcon className="h-4 w-4 text-zinc-400" />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="-left-10 select-none rounded-md bg-zinc-800/80 px-3.5 py-1.5 text-sm leading-none text-zinc-100 backdrop-blur-lg 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"
|
||||
sideOffset={5}
|
||||
>
|
||||
Open thread
|
||||
<Tooltip.Arrow className="fill-zinc-800/80 backdrop-blur-lg" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
<div>
|
||||
{data.replies > 0 ? (
|
||||
<>
|
||||
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
|
||||
<div className="relative z-10 flex items-center gap-3 bg-zinc-900 pb-3">
|
||||
<div className="mt-2 inline-flex h-6 w-11 shrink-0 items-center justify-center">
|
||||
<div className="isolate flex -space-x-1 overflow-hidden">
|
||||
{data.users?.map((user, index) => (
|
||||
<MiniUser key={user + index} pubkey={user} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 inline-flex h-6 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openThread(id)}
|
||||
className="text-zinc-500"
|
||||
>
|
||||
<span className="font-semibold text-zinc-300">{data.replies}</span>{' '}
|
||||
replies
|
||||
</button>
|
||||
<span className="text-zinc-500">·</span>
|
||||
<p className="text-zinc-500">
|
||||
<span className="font-semibold text-zinc-300">
|
||||
{compactNumber.format(data.zap)}
|
||||
</span>{' '}
|
||||
zaps
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="pb-3" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Kind1 } from '@shared/notes/contents/kind1';
|
||||
import { Kind1063 } from '@shared/notes/contents/kind1063';
|
||||
import { NoteMetadata } from '@shared/notes/metadata';
|
||||
import { NoteParent } from '@shared/notes/parent';
|
||||
import { Repost } from '@shared/notes/repost';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { parser } from '@utils/parser';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
interface Note {
|
||||
event: LumeEvent;
|
||||
skipMetadata?: boolean;
|
||||
}
|
||||
|
||||
export function Note({ event, skipMetadata = false }: Note) {
|
||||
const isRepost = event.kind === 6;
|
||||
|
||||
const renderParent = useMemo(() => {
|
||||
if (!isRepost && event.parent_id && event.parent_id !== event.event_id) {
|
||||
return <NoteParent id={event.parent_id} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [event.parent_id]);
|
||||
|
||||
const renderRepost = useMemo(() => {
|
||||
if (isRepost) {
|
||||
return <Repost event={event} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [event.kind]);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
switch (event.kind) {
|
||||
case 1: {
|
||||
const content = parser(event);
|
||||
return <Kind1 content={content} />;
|
||||
}
|
||||
case 6:
|
||||
return null;
|
||||
case 1063:
|
||||
return <Kind1063 metadata={event.tags} />;
|
||||
default:
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="inline-flex flex-col gap-1 rounded-md bg-zinc-800 px-2 py-2">
|
||||
<span className="text-sm font-medium leading-none text-zinc-500">
|
||||
Kind: {event.kind}
|
||||
</span>
|
||||
<p className="text-sm leading-none text-fuchsia-500">
|
||||
Lume isn't fully support this kind in newsfeed
|
||||
</p>
|
||||
</div>
|
||||
<div className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
<p>{event.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [event.kind]);
|
||||
|
||||
return (
|
||||
<div className="h-min w-full px-3 py-1.5">
|
||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||
{renderParent}
|
||||
<div className="flex flex-col">
|
||||
<User pubkey={event.pubkey} time={event.created_at} repost={isRepost} />
|
||||
<div className="-mt-6 pl-[49px]">
|
||||
{renderContent}
|
||||
{!isRepost && !skipMetadata ? (
|
||||
<NoteMetadata
|
||||
id={event.event_id}
|
||||
rootID={event.parent_id}
|
||||
eventPubkey={event.pubkey}
|
||||
/>
|
||||
) : (
|
||||
<div className={isRepost ? 'h-0' : 'h-3'} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{renderRepost}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Kind1 } from '@shared/notes/contents/kind1';
|
||||
import { Kind1063 } from '@shared/notes/contents/kind1063';
|
||||
import { NoteMetadata } from '@shared/notes/metadata';
|
||||
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useEvent } from '@utils/hooks/useEvent';
|
||||
|
||||
export function NoteParent({ id }: { id: string }) {
|
||||
const { status, data } = useEvent(id);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col pb-6">
|
||||
<div className="absolute left-[18px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
|
||||
{status === 'loading' ? (
|
||||
<NoteSkeleton />
|
||||
) : status === 'success' ? (
|
||||
<>
|
||||
<User pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="-mt-6 pl-[49px]">
|
||||
{data.kind === 1 && <Kind1 content={data.content} />}
|
||||
{data.kind === 1063 && <Kind1063 metadata={data.tags} />}
|
||||
{data.kind !== 1 && data.kind !== 1063 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="inline-flex flex-col gap-1 rounded-md bg-zinc-800 px-2 py-2">
|
||||
<span className="text-sm font-medium leading-none text-zinc-500">
|
||||
Kind: {data.kind}
|
||||
</span>
|
||||
<p className="text-sm leading-none text-fuchsia-500">
|
||||
Lume isn't fully support this kind in newsfeed
|
||||
</p>
|
||||
</div>
|
||||
<div className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
<p>{data.content || data.toString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<NoteMetadata id={data.event_id || data.id} eventPubkey={data.pubkey} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>Failed to fetch event</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Kind1 } from '@shared/notes/contents/kind1';
|
||||
import { NoteMetadata } from '@shared/notes/metadata';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
@@ -12,7 +11,6 @@ export function Reply({ data }: { data: any }) {
|
||||
<div className="flex flex-col">
|
||||
<User pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="-mt-[20px] pl-[50px]">
|
||||
<Kind1 content={content} />
|
||||
<NoteMetadata id={data.event_id} eventPubkey={data.pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Kind1 } from '@shared/notes/contents/kind1';
|
||||
import { Kind1063 } from '@shared/notes/contents/kind1063';
|
||||
import { NoteMetadata } from '@shared/notes/metadata';
|
||||
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useEvent } from '@utils/hooks/useEvent';
|
||||
import { getRepostID } from '@utils/transform';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function Repost({ event }: { event: LumeEvent }) {
|
||||
const repostID = getRepostID(event.tags);
|
||||
const { status, data } = useEvent(repostID);
|
||||
|
||||
return (
|
||||
<div className="relative mt-12 flex flex-col">
|
||||
<div className="absolute -top-10 left-[18px] h-[50px] w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
|
||||
{status === 'loading' ? (
|
||||
<NoteSkeleton />
|
||||
) : status === 'success' ? (
|
||||
<>
|
||||
<User pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="-mt-6 pl-[49px]">
|
||||
{data.kind === 1 && <Kind1 content={data.content} />}
|
||||
{data.kind === 1063 && <Kind1063 metadata={data.tags} />}
|
||||
{data.kind !== 1 && data.kind !== 1063 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="inline-flex flex-col gap-1 rounded-md bg-zinc-800 px-2 py-2">
|
||||
<span className="text-sm font-medium leading-none text-zinc-500">
|
||||
Kind: {data.kind}
|
||||
</span>
|
||||
<p className="text-sm leading-none text-fuchsia-500">
|
||||
Lume isn't fully support this kind in newsfeed
|
||||
</p>
|
||||
</div>
|
||||
<div className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
<p>{data.content || data.toString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<NoteMetadata id={data.event_id || data.id} eventPubkey={data.pubkey} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>Failed to fetch event</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/shared/notes/users/mini.tsx
Normal file
22
src/shared/notes/users/mini.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useProfile } from '@utils/hooks/useProfile';
|
||||
|
||||
export function MiniUser({ pubkey }: { pubkey: string }) {
|
||||
const { status, user } = useProfile(pubkey);
|
||||
|
||||
if (status === 'loading') {
|
||||
return <div className="h-4 w-4 animate-pulse rounded bg-zinc-700"></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={user?.picture || user?.image || DEFAULT_AVATAR}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="relative z-20 inline-block h-4 w-4 rounded bg-white ring-1 ring-zinc-800"
|
||||
/>
|
||||
);
|
||||
}
|
||||
34
src/shared/notes/users/repost.tsx
Normal file
34
src/shared/notes/users/repost.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useProfile } from '@utils/hooks/useProfile';
|
||||
import { shortenKey } from '@utils/shortenKey';
|
||||
|
||||
export function RepostUser({ pubkey }: { pubkey: string }) {
|
||||
const { status, user } = useProfile(pubkey);
|
||||
|
||||
if (status === 'loading') {
|
||||
return <div className="h-4 w-4 animate-pulse rounded bg-zinc-700"></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 pl-6">
|
||||
<Image
|
||||
src={user?.picture || user?.image || DEFAULT_AVATAR}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="relative z-20 inline-block h-6 w-6 rounded bg-white ring-1 ring-zinc-800"
|
||||
/>
|
||||
<div className="inline-flex items-baseline gap-1">
|
||||
<h5 className="max-w-[18rem] truncate text-zinc-400">
|
||||
{user?.nip05?.toLowerCase() ||
|
||||
user?.name ||
|
||||
user?.display_name ||
|
||||
shortenKey(pubkey)}
|
||||
</h5>
|
||||
<span className="text-zinc-400">reposted</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user