From 6c91ba9effbe3248ef3ee99839846071072f87e4 Mon Sep 17 00:00:00 2001 From: Cody Tseng Date: Thu, 22 May 2025 22:39:13 +0800 Subject: [PATCH] feat: highlight (#346) --- src/components/ContentPreview/index.tsx | 6 +- src/components/Note/Highlight.tsx | 129 ++++++++++++++++++++++++ src/components/Note/index.tsx | 29 ++++-- src/components/PostEditor/Preview.tsx | 15 +-- src/lib/event.ts | 15 ++- src/lib/link.ts | 1 + src/lib/tag.ts | 15 +++ src/pages/secondary/NotePage/index.tsx | 16 ++- 8 files changed, 197 insertions(+), 29 deletions(-) create mode 100644 src/components/Note/Highlight.tsx diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx index 3cf5acfe..00d92ebc 100644 --- a/src/components/ContentPreview/index.tsx +++ b/src/components/ContentPreview/index.tsx @@ -16,10 +16,12 @@ import Emoji from '../Emoji' export default function ContentPreview({ event, - className + className, + onClick }: { event?: Event className?: string + onClick?: React.MouseEventHandler | undefined }) { const { t } = useTranslation() const nodes = useMemo(() => { @@ -37,7 +39,7 @@ export default function ContentPreview({ const emojiInfos = extractEmojiInfosFromTags(event?.tags) return ( -
+
{nodes.map((node, index) => { if (node.type === 'text') { return node.data diff --git a/src/components/Note/Highlight.tsx b/src/components/Note/Highlight.tsx new file mode 100644 index 00000000..17f8e28e --- /dev/null +++ b/src/components/Note/Highlight.tsx @@ -0,0 +1,129 @@ +import { useFetchEvent } from '@/hooks' +import { createFakeEvent, isSupportedKind } from '@/lib/event' +import { toNjump, toNote } from '@/lib/link' +import { isValidPubkey } from '@/lib/pubkey' +import { generateEventIdFromATag } from '@/lib/tag' +import { cn } from '@/lib/utils' +import { useSecondaryPage } from '@/PageManager' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import Content from '../Content' +import ContentPreview from '../ContentPreview' +import UserAvatar from '../UserAvatar' + +export default function Highlight({ event, className }: { event: Event; className?: string }) { + const comment = useMemo(() => event.tags.find((tag) => tag[0] === 'comment')?.[1], [event]) + + return ( +
+ {comment && } +
+
+
{event.content}
+
+ +
+ ) +} + +function HighlightSource({ event }: { event: Event }) { + const { push } = useSecondaryPage() + const sourceTag = useMemo(() => { + let sourceTag: string[] | undefined + for (const tag of event.tags) { + if (tag[2] === 'source') { + sourceTag = tag + break + } + if (tag[0] === 'r') { + sourceTag = tag + continue + } else if (tag[0] === 'a') { + if (!sourceTag || sourceTag[0] !== 'r') { + sourceTag = tag + } + continue + } else if (tag[0] === 'e') { + if (!sourceTag || sourceTag[0] === 'e') { + sourceTag = tag + } + continue + } + } + + return sourceTag + }, [event]) + const { event: referenceEvent } = useFetchEvent( + sourceTag && sourceTag[0] === 'e' ? sourceTag[1] : undefined + ) + const referenceEventId = useMemo(() => { + if (!sourceTag || sourceTag[0] === 'r') return + if (sourceTag[0] === 'e') { + return sourceTag[1] + } + if (sourceTag[0] === 'a') { + return generateEventIdFromATag(sourceTag) + } + }, [sourceTag]) + const pubkey = useMemo(() => { + if (referenceEvent) { + return referenceEvent.pubkey + } + if (sourceTag && sourceTag[0] === 'a') { + const [, pubkey] = sourceTag[1].split(':') + if (isValidPubkey(pubkey)) { + return pubkey + } + } + }, [sourceTag, referenceEvent]) + + if (!sourceTag) { + return null + } + + if (sourceTag[0] === 'r') { + return ( + + ) + } + + return ( +
+
{'From'}
+ {pubkey && } + {referenceEvent && isSupportedKind(referenceEvent.kind) ? ( + { + e.stopPropagation() + push(toNote(referenceEvent)) + }} + /> + ) : referenceEventId ? ( + + ) : null} +
+ ) +} diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index a80e575c..89766fa1 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -1,8 +1,12 @@ import { useSecondaryPage } from '@/PageManager' -import { ExtendedKind } from '@/constants' -import { extractImageInfosFromEventTags, getParentEventId, getUsingClient } from '@/lib/event' +import { + extractImageInfosFromEventTags, + getParentEventId, + getUsingClient, + isPictureEvent +} from '@/lib/event' import { toNote } from '@/lib/link' -import { Event } from 'nostr-tools' +import { Event, kinds } from 'nostr-tools' import { useMemo } from 'react' import Content from '../Content' import { FormattedTimestamp } from '../FormattedTimestamp' @@ -11,6 +15,7 @@ import NoteOptions from '../NoteOptions' import ParentNotePreview from '../ParentNotePreview' import UserAvatar from '../UserAvatar' import Username from '../Username' +import Highlight from './Highlight' export default function Note({ event, @@ -25,10 +30,16 @@ export default function Note({ }) { const { push } = useSecondaryPage() const parentEventId = useMemo( - () => (hideParentNotePreview ? undefined : getParentEventId(event)), + () => + !hideParentNotePreview && event.kind === kinds.ShortTextNote + ? getParentEventId(event) + : undefined, [event, hideParentNotePreview] ) - const imageInfos = useMemo(() => extractImageInfosFromEventTags(event), [event]) + const imageInfos = useMemo( + () => (isPictureEvent(event) ? extractImageInfosFromEventTags(event) : []), + [event] + ) const usingClient = useMemo(() => getUsingClient(event), [event]) return ( @@ -66,10 +77,12 @@ export default function Note({ }} /> )} - - {event.kind === ExtendedKind.PICTURE && imageInfos.length > 0 && ( - + {event.kind === kinds.Highlights ? ( + + ) : ( + )} + {imageInfos.length > 0 && }
) } diff --git a/src/components/PostEditor/Preview.tsx b/src/components/PostEditor/Preview.tsx index 5b96dad7..85db71db 100644 --- a/src/components/PostEditor/Preview.tsx +++ b/src/components/PostEditor/Preview.tsx @@ -1,22 +1,11 @@ import { Card } from '@/components/ui/card' -import dayjs from 'dayjs' +import { createFakeEvent } from '@/lib/event' import Content from '../Content' export default function Preview({ content }: { content: string }) { return ( - + ) } diff --git a/src/lib/event.ts b/src/lib/event.ts index 7d9f8aa6..d7cf1430 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -60,7 +60,7 @@ export function isProtectedEvent(event: Event) { } export function isSupportedKind(kind: number) { - return [kinds.ShortTextNote, ExtendedKind.PICTURE].includes(kind) + return [kinds.ShortTextNote, kinds.Highlights, ExtendedKind.PICTURE].includes(kind) } export function getParentEventTag(event?: Event) { @@ -524,3 +524,16 @@ export function extractEmojiInfosFromTags(tags: string[][] = []) { }) .filter(Boolean) as TEmoji[] } + +export function createFakeEvent(event: Partial): Event { + return { + id: '', + kind: 1, + pubkey: '', + content: '', + created_at: 0, + tags: [], + sig: '', + ...event + } +} diff --git a/src/lib/link.ts b/src/lib/link.ts index 308e5d33..222b9795 100644 --- a/src/lib/link.ts +++ b/src/lib/link.ts @@ -52,3 +52,4 @@ export const toZapStreamLiveEvent = (event: Event) => { return `https://zap.stream/${getSharableEventId(event)}` } export const toChachiChat = (relay: string, d: string) => `https://chachi.chat/${relay}/${d}` +export const toNjump = (id: string) => `https://njump.me/${id}` diff --git a/src/lib/tag.ts b/src/lib/tag.ts index a05a3ea8..a3155d7b 100644 --- a/src/lib/tag.ts +++ b/src/lib/tag.ts @@ -29,6 +29,21 @@ export function generateEventIdFromETag(tag: string[]) { } } +export function generateEventIdFromATag(tag: string[]) { + try { + const [, coordinate, relay] = tag + const [kind, pubkey, identifier] = coordinate.split(':') + return nip19.naddrEncode({ + kind: Number(kind), + pubkey, + identifier, + relays: relay ? [relay] : undefined + }) + } catch { + return undefined + } +} + export function generateEventId(event: Pick) { const relay = client.getEventHint(event.id) return nip19.neventEncode({ id: event.id, author: event.pubkey, relays: [relay] }) diff --git a/src/pages/secondary/NotePage/index.tsx b/src/pages/secondary/NotePage/index.tsx index 850becd3..48bf77ad 100644 --- a/src/pages/secondary/NotePage/index.tsx +++ b/src/pages/secondary/NotePage/index.tsx @@ -22,8 +22,14 @@ import NotFoundPage from '../NotFoundPage' const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => { const { t } = useTranslation() const { event, isFetching } = useFetchEvent(id) - const parentEventId = useMemo(() => getParentEventId(event), [event]) - const rootEventId = useMemo(() => getRootEventId(event), [event]) + const parentEventId = useMemo( + () => (event?.kind === kinds.ShortTextNote ? getParentEventId(event) : undefined), + [event] + ) + const rootEventId = useMemo( + () => (event?.kind === kinds.ShortTextNote ? getRootEventId(event) : undefined), + [event] + ) if (!event && isFetching) { return ( @@ -80,11 +86,11 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
- {event.kind === kinds.ShortTextNote ? ( + {[kinds.ShortTextNote, kinds.Highlights].includes(event.kind) ? ( - ) : isPictureEvent(event) ? ( + ) : ( - ) : null} + )} ) })