feat: optimize the display effect of other kinds of events
This commit is contained in:
@@ -1,30 +1,69 @@
|
|||||||
import { PICTURE_EVENT_KIND } from '@/constants'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { useFetchEvent } from '@/hooks'
|
import { useFetchEvent } from '@/hooks'
|
||||||
import { toNoStrudelArticle, toNoStrudelNote, toNoStrudelStream } from '@/lib/link'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { kinds } from 'nostr-tools'
|
import { Check, Copy } from 'lucide-react'
|
||||||
import NormalNoteCard from '../NoteCard/NormalNoteCard'
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import GenericNoteCard from '../NoteCard/GenericNoteCard'
|
||||||
|
|
||||||
export function EmbeddedNote({ noteId, className }: { noteId: string; className?: string }) {
|
export function EmbeddedNote({ noteId, className }: { noteId: string; className?: string }) {
|
||||||
const { event } = useFetchEvent(noteId)
|
const { event, isFetching } = useFetchEvent(noteId)
|
||||||
|
|
||||||
return event && [kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(event.kind) ? (
|
if (isFetching) {
|
||||||
<NormalNoteCard className={cn('w-full', className)} event={event} embedded />
|
return <EmbeddedNoteSkeleton className={className} />
|
||||||
) : (
|
|
||||||
<a
|
|
||||||
href={
|
|
||||||
event?.kind === kinds.LongFormArticle
|
|
||||||
? toNoStrudelArticle(noteId)
|
|
||||||
: event?.kind === kinds.LiveEvent
|
|
||||||
? toNoStrudelStream(noteId)
|
|
||||||
: toNoStrudelNote(noteId)
|
|
||||||
}
|
}
|
||||||
target="_blank"
|
|
||||||
className="text-highlight hover:underline"
|
if (!event) {
|
||||||
onClick={(e) => e.stopPropagation()}
|
return <EmbeddedNoteNotFound className={className} noteId={noteId} />
|
||||||
rel="noreferrer"
|
}
|
||||||
>
|
|
||||||
{noteId}
|
return (
|
||||||
</a>
|
<GenericNoteCard
|
||||||
|
className={cn('w-full', className)}
|
||||||
|
event={event}
|
||||||
|
embedded
|
||||||
|
originalNoteId={noteId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmbeddedNoteSkeleton({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('text-left p-2 sm:p-3 border rounded-lg', className)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Skeleton className="w-7 h-7 rounded-full" />
|
||||||
|
<Skeleton className="h-3 w-16 my-1" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="w-full h-4 my-1 mt-2" />
|
||||||
|
<Skeleton className="w-2/3 h-4 my-1" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmbeddedNoteNotFound({ noteId, className }: { noteId: string; className?: string }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('text-left p-2 sm:p-3 border rounded-lg', className)}>
|
||||||
|
<div className="flex flex-col items-center text-muted-foreground font-medium gap-2">
|
||||||
|
<div>{t('Sorry! The note cannot be found 😔')}</div>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
navigator.clipboard.writeText(noteId)
|
||||||
|
setIsCopied(true)
|
||||||
|
setTimeout(() => setIsCopied(false), 2000)
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
{isCopied ? <Check /> : <Copy />} Copy note ID
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export default function Note({
|
|||||||
}) {
|
}) {
|
||||||
const { push } = useSecondaryPage()
|
const { push } = useSecondaryPage()
|
||||||
const usingClient = useMemo(() => getUsingClient(event), [event])
|
const usingClient = useMemo(() => getUsingClient(event), [event])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|||||||
56
src/components/NoteCard/GenericNoteCard.tsx
Normal file
56
src/components/NoteCard/GenericNoteCard.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { GROUP_METADATA_EVENT_KIND } from '@/constants'
|
||||||
|
import { isSupportedKind } from '@/lib/event'
|
||||||
|
import { Event, kinds } from 'nostr-tools'
|
||||||
|
import GroupMetadataCard from './GroupMetadataCard'
|
||||||
|
import LiveEventCard from './LiveEventCard'
|
||||||
|
import LongFormArticleCard from './LongFormArticleCard'
|
||||||
|
import MainNoteCard from './MainNoteCard'
|
||||||
|
import UnknownNoteCard from './UnknownNoteCard'
|
||||||
|
|
||||||
|
export default function GenericNoteCard({
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
reposter,
|
||||||
|
embedded,
|
||||||
|
originalNoteId
|
||||||
|
}: {
|
||||||
|
event: Event
|
||||||
|
className?: string
|
||||||
|
reposter?: string
|
||||||
|
embedded?: boolean
|
||||||
|
originalNoteId?: string
|
||||||
|
}) {
|
||||||
|
if (isSupportedKind(event.kind)) {
|
||||||
|
return (
|
||||||
|
<MainNoteCard event={event} className={className} reposter={reposter} embedded={embedded} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (event.kind === kinds.LongFormArticle) {
|
||||||
|
return (
|
||||||
|
<LongFormArticleCard
|
||||||
|
className={className}
|
||||||
|
reposter={reposter}
|
||||||
|
event={event}
|
||||||
|
embedded={embedded}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (event.kind === kinds.LiveEvent) {
|
||||||
|
return (
|
||||||
|
<LiveEventCard event={event} className={className} reposter={reposter} embedded={embedded} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (event.kind === GROUP_METADATA_EVENT_KIND) {
|
||||||
|
return (
|
||||||
|
<GroupMetadataCard
|
||||||
|
className={className}
|
||||||
|
event={event}
|
||||||
|
originalNoteId={originalNoteId}
|
||||||
|
embedded={embedded}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<UnknownNoteCard event={event} className={className} reposter={reposter} embedded={embedded} />
|
||||||
|
)
|
||||||
|
}
|
||||||
147
src/components/NoteCard/GroupMetadataCard.tsx
Normal file
147
src/components/NoteCard/GroupMetadataCard.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { getSharableEventId } from '@/lib/event'
|
||||||
|
import { toChachiChat } from '@/lib/link'
|
||||||
|
import { simplifyUrl } from '@/lib/url'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
|
import client from '@/services/client.service'
|
||||||
|
import { Check, Copy, ExternalLink } from 'lucide-react'
|
||||||
|
import { Event, nip19 } from 'nostr-tools'
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { FormattedTimestamp } from '../FormattedTimestamp'
|
||||||
|
import Image from '../Image'
|
||||||
|
import UserAvatar from '../UserAvatar'
|
||||||
|
import Username from '../Username'
|
||||||
|
import RepostDescription from './RepostDescription'
|
||||||
|
|
||||||
|
export default function GroupMetadataCard({
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
originalNoteId,
|
||||||
|
embedded = false,
|
||||||
|
reposter
|
||||||
|
}: {
|
||||||
|
event: Event
|
||||||
|
className?: string
|
||||||
|
originalNoteId?: string
|
||||||
|
embedded?: boolean
|
||||||
|
reposter?: string
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
|
const metadata = useMemo(() => {
|
||||||
|
let d: string | undefined
|
||||||
|
let name: string | undefined
|
||||||
|
let about: string | undefined
|
||||||
|
let picture: string | undefined
|
||||||
|
let relay: string | undefined
|
||||||
|
const tags = new Set<string>()
|
||||||
|
|
||||||
|
if (originalNoteId) {
|
||||||
|
const pointer = nip19.decode(originalNoteId)
|
||||||
|
if (pointer.type === 'naddr' && pointer.data.relays?.length) {
|
||||||
|
relay = pointer.data.relays[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!relay) {
|
||||||
|
relay = client.getEventHint(event.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
event.tags.forEach(([tagName, tagValue]) => {
|
||||||
|
if (tagName === 'name') {
|
||||||
|
name = tagValue
|
||||||
|
} else if (tagName === 'about') {
|
||||||
|
about = tagValue
|
||||||
|
} else if (tagName === 'picture') {
|
||||||
|
picture = tagValue
|
||||||
|
} else if (tagName === 't' && tagValue) {
|
||||||
|
tags.add(tagValue.toLocaleLowerCase())
|
||||||
|
} else if (tagName === 'd') {
|
||||||
|
d = tagValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
name = d ?? 'no name'
|
||||||
|
}
|
||||||
|
|
||||||
|
return { d, name, about, picture, tags: Array.from(tags), relay }
|
||||||
|
}, [event, originalNoteId])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative', className)}>
|
||||||
|
<div className={cn(embedded ? 'p-2 sm:p-3 border rounded-lg' : 'px-4 py-3')}>
|
||||||
|
<RepostDescription reposter={reposter} />
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<UserAvatar userId={event.pubkey} size={embedded ? 'small' : 'normal'} />
|
||||||
|
<div
|
||||||
|
className={`flex-1 w-0 ${embedded ? 'flex space-x-2 items-center overflow-hidden' : ''}`}
|
||||||
|
>
|
||||||
|
<Username
|
||||||
|
userId={event.pubkey}
|
||||||
|
className={cn('font-semibold flex truncate', embedded ? 'text-sm' : '')}
|
||||||
|
skeletonClassName={embedded ? 'h-3' : 'h-4'}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground line-clamp-1">
|
||||||
|
<FormattedTimestamp timestamp={event.created_at} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-start mt-2">
|
||||||
|
{metadata.picture && (
|
||||||
|
<Image image={{ url: metadata.picture }} className="h-32 aspect-square rounded-lg" />
|
||||||
|
)}
|
||||||
|
<div className="flex-1 w-0 space-y-1">
|
||||||
|
<div className="text-xl font-semibold line-clamp-1">{metadata.name}</div>
|
||||||
|
{metadata.about && (
|
||||||
|
<div className="text-sm text-muted-foreground line-clamp-4">{metadata.about}</div>
|
||||||
|
)}
|
||||||
|
{metadata.tags.length > 0 && (
|
||||||
|
<div className="mt-2 flex gap-1 flex-wrap">
|
||||||
|
{metadata.tags.map((tag) => (
|
||||||
|
<Badge key={tag} variant="secondary">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(!metadata.relay || !metadata.d) && (
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
navigator.clipboard.writeText(originalNoteId ?? getSharableEventId(event))
|
||||||
|
setIsCopied(true)
|
||||||
|
setTimeout(() => setIsCopied(false), 2000)
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
{isCopied ? <Check /> : <Copy />} Copy group ID
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!embedded && <Separator />}
|
||||||
|
{!isSmallScreen && metadata.relay && metadata.d && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute top-0 w-full h-full bg-muted/80 backdrop-blur-sm flex flex-col items-center justify-center cursor-pointer transition-opacity opacity-0 hover:opacity-100',
|
||||||
|
embedded ? 'rounded-lg' : ''
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
window.open(toChachiChat(simplifyUrl(metadata.relay), metadata.d!), '_blank')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center font-semibold">
|
||||||
|
<ExternalLink className="size-4" /> {t('Open in a', { a: 'Chachi' })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
168
src/components/NoteCard/LiveEventCard.tsx
Normal file
168
src/components/NoteCard/LiveEventCard.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { toZapStreamLiveEvent } from '@/lib/link'
|
||||||
|
import { tagNameEquals } from '@/lib/tag'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
|
import { ExternalLink } from 'lucide-react'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { FormattedTimestamp } from '../FormattedTimestamp'
|
||||||
|
import Image from '../Image'
|
||||||
|
import UserAvatar from '../UserAvatar'
|
||||||
|
import Username from '../Username'
|
||||||
|
import RepostDescription from './RepostDescription'
|
||||||
|
|
||||||
|
export default function LiveEventCard({
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
embedded = false,
|
||||||
|
reposter
|
||||||
|
}: {
|
||||||
|
event: Event
|
||||||
|
className?: string
|
||||||
|
embedded?: boolean
|
||||||
|
reposter?: string
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
const metadata = useMemo(() => {
|
||||||
|
let title: string | undefined
|
||||||
|
let summary: string | undefined
|
||||||
|
let image: string | undefined
|
||||||
|
let status: string | undefined
|
||||||
|
const tags = new Set<string>()
|
||||||
|
|
||||||
|
event.tags.forEach(([tagName, tagValue]) => {
|
||||||
|
if (tagName === 'title') {
|
||||||
|
title = tagValue
|
||||||
|
} else if (tagName === 'summary') {
|
||||||
|
summary = tagValue
|
||||||
|
} else if (tagName === 'image') {
|
||||||
|
image = tagValue
|
||||||
|
} else if (tagName === 'status') {
|
||||||
|
status = tagValue
|
||||||
|
} else if (tagName === 't' && tagValue && tags.size < 6) {
|
||||||
|
tags.add(tagValue.toLocaleLowerCase())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
title = event.tags.find(tagNameEquals('d'))?.[1] ?? 'no title'
|
||||||
|
}
|
||||||
|
|
||||||
|
return { title, summary, image, status, tags: Array.from(tags) }
|
||||||
|
}, [event])
|
||||||
|
|
||||||
|
const liveStatusComponent =
|
||||||
|
metadata.status &&
|
||||||
|
(metadata.status === 'live' ? (
|
||||||
|
<Badge className="bg-green-400 hover:bg-green-400">live</Badge>
|
||||||
|
) : metadata.status === 'ended' ? (
|
||||||
|
<Badge variant="destructive">ended</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary">{metadata.status}</Badge>
|
||||||
|
))
|
||||||
|
|
||||||
|
const userInfoComponent = (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<UserAvatar userId={event.pubkey} size={embedded ? 'small' : 'normal'} />
|
||||||
|
<div
|
||||||
|
className={`flex-1 w-0 ${embedded ? 'flex space-x-2 items-center overflow-hidden' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Username
|
||||||
|
userId={event.pubkey}
|
||||||
|
className={cn('font-semibold flex truncate', embedded ? 'text-sm' : '')}
|
||||||
|
skeletonClassName={embedded ? 'h-3' : 'h-4'}
|
||||||
|
/>
|
||||||
|
{liveStatusComponent}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground line-clamp-1">
|
||||||
|
<FormattedTimestamp timestamp={event.created_at} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const titleComponent = <div className="text-xl font-semibold line-clamp-1">{metadata.title}</div>
|
||||||
|
|
||||||
|
const summaryComponent = metadata.summary && (
|
||||||
|
<div className="text-sm text-muted-foreground line-clamp-4">{metadata.summary}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const tagsComponent = metadata.tags.length > 0 && (
|
||||||
|
<div className="flex gap-1 flex-wrap">
|
||||||
|
{metadata.tags.map((tag) => (
|
||||||
|
<Badge key={tag} variant="secondary">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
window.open(toZapStreamLiveEvent(event), '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-col gap-2', embedded ? 'p-2 border rounded-lg' : 'px-4 py-3')}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<RepostDescription reposter={reposter} />
|
||||||
|
{userInfoComponent}
|
||||||
|
{metadata.image && (
|
||||||
|
<Image
|
||||||
|
image={{ url: metadata.image }}
|
||||||
|
className="w-full aspect-video object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{titleComponent}
|
||||||
|
{summaryComponent}
|
||||||
|
{tagsComponent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!embedded && <Separator />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative', className)}>
|
||||||
|
<div
|
||||||
|
className={cn('flex gap-2 items-center', embedded ? 'p-3 border rounded-lg' : 'px-4 py-3')}
|
||||||
|
>
|
||||||
|
<div className="flex-1 w-0">
|
||||||
|
<RepostDescription reposter={reposter} />
|
||||||
|
{userInfoComponent}
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{titleComponent}
|
||||||
|
{summaryComponent}
|
||||||
|
{tagsComponent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{metadata.image && (
|
||||||
|
<Image image={{ url: metadata.image }} className="h-36 max-w-44 rounded-lg" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!embedded && <Separator />}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute top-0 w-full h-full bg-muted/80 backdrop-blur-sm flex flex-col items-center justify-center cursor-pointer transition-opacity opacity-0 hover:opacity-100',
|
||||||
|
embedded ? 'rounded-lg' : ''
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center font-semibold">
|
||||||
|
<ExternalLink className="size-4" /> {t('Open in a', { a: 'Zap Stream' })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
161
src/components/NoteCard/LongFormArticleCard.tsx
Normal file
161
src/components/NoteCard/LongFormArticleCard.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { toHablaLongFormArticle } from '@/lib/link'
|
||||||
|
import { tagNameEquals } from '@/lib/tag'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
|
import { ExternalLink } from 'lucide-react'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Image from '../Image'
|
||||||
|
import UserAvatar from '../UserAvatar'
|
||||||
|
import Username from '../Username'
|
||||||
|
import RepostDescription from './RepostDescription'
|
||||||
|
|
||||||
|
export default function LongFormArticleCard({
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
embedded = false,
|
||||||
|
reposter
|
||||||
|
}: {
|
||||||
|
event: Event
|
||||||
|
className?: string
|
||||||
|
embedded?: boolean
|
||||||
|
reposter?: string
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
const metadata = useMemo(() => {
|
||||||
|
let title: string | undefined
|
||||||
|
let summary: string | undefined
|
||||||
|
let image: string | undefined
|
||||||
|
let publishDateString: string | undefined
|
||||||
|
const tags = new Set<string>()
|
||||||
|
|
||||||
|
event.tags.forEach(([tagName, tagValue]) => {
|
||||||
|
if (tagName === 'title') {
|
||||||
|
title = tagValue
|
||||||
|
} else if (tagName === 'summary') {
|
||||||
|
summary = tagValue
|
||||||
|
} else if (tagName === 'image') {
|
||||||
|
image = tagValue
|
||||||
|
} else if (tagName === 'published_at') {
|
||||||
|
try {
|
||||||
|
const publishedAt = parseInt(tagValue)
|
||||||
|
publishDateString = !isNaN(publishedAt)
|
||||||
|
? new Date(publishedAt * 1000).toLocaleString()
|
||||||
|
: undefined
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
} else if (tagName === 't' && tagValue && tags.size < 6) {
|
||||||
|
tags.add(tagValue.toLocaleLowerCase())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
title = event.tags.find(tagNameEquals('d'))?.[1] ?? 'no title'
|
||||||
|
}
|
||||||
|
|
||||||
|
return { title, summary, image, publishDateString, tags: Array.from(tags) }
|
||||||
|
}, [event])
|
||||||
|
|
||||||
|
const userInfoComponent = (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<UserAvatar userId={event.pubkey} size={embedded ? 'small' : 'normal'} />
|
||||||
|
<div
|
||||||
|
className={`flex-1 w-0 ${embedded ? 'flex space-x-2 items-center overflow-hidden' : ''}`}
|
||||||
|
>
|
||||||
|
<Username
|
||||||
|
userId={event.pubkey}
|
||||||
|
className={cn('font-semibold flex truncate', embedded ? 'text-sm' : '')}
|
||||||
|
skeletonClassName={embedded ? 'h-3' : 'h-4'}
|
||||||
|
/>
|
||||||
|
{metadata.publishDateString && (
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">{metadata.publishDateString}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const titleComponent = <div className="text-xl font-semibold line-clamp-2">{metadata.title}</div>
|
||||||
|
|
||||||
|
const tagsComponent = metadata.tags.length > 0 && (
|
||||||
|
<div className="flex gap-1 flex-wrap">
|
||||||
|
{metadata.tags.map((tag) => (
|
||||||
|
<Badge key={tag} variant="secondary">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const summaryComponent = metadata.summary && (
|
||||||
|
<div className="text-sm text-muted-foreground line-clamp-4">{metadata.summary}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
window.open(toHablaLongFormArticle(event), '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-col gap-2', embedded ? 'p-2 border rounded-lg' : 'px-4 py-3')}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<RepostDescription reposter={reposter} />
|
||||||
|
{userInfoComponent}
|
||||||
|
{metadata.image && (
|
||||||
|
<Image
|
||||||
|
image={{ url: metadata.image }}
|
||||||
|
className="w-full aspect-video object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{titleComponent}
|
||||||
|
{tagsComponent}
|
||||||
|
{summaryComponent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!embedded && <Separator />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative', className)}>
|
||||||
|
<div
|
||||||
|
className={cn('flex gap-2 items-center', embedded ? 'p-3 border rounded-lg' : 'px-4 py-3')}
|
||||||
|
>
|
||||||
|
<div className="flex-1 w-0">
|
||||||
|
<RepostDescription reposter={reposter} />
|
||||||
|
{userInfoComponent}
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{titleComponent}
|
||||||
|
{tagsComponent}
|
||||||
|
{summaryComponent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{metadata.image && (
|
||||||
|
<Image image={{ url: metadata.image }} className="h-36 max-w-48 rounded-lg" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!embedded && <Separator />}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute top-0 w-full h-full bg-muted/60 backdrop-blur-sm flex flex-col items-center justify-center cursor-pointer transition-opacity opacity-0 hover:opacity-100',
|
||||||
|
embedded ? 'rounded-lg' : ''
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center font-semibold">
|
||||||
|
<ExternalLink className="size-4" /> {t('Open in a', { a: 'Habla' })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,15 +2,12 @@ import { Separator } from '@/components/ui/separator'
|
|||||||
import { useFetchEvent } from '@/hooks'
|
import { useFetchEvent } from '@/hooks'
|
||||||
import { getParentEventId, getRootEventId } from '@/lib/event'
|
import { getParentEventId, getRootEventId } from '@/lib/event'
|
||||||
import { toNote } from '@/lib/link'
|
import { toNote } from '@/lib/link'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { useSecondaryPage } from '@/PageManager'
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
import { Repeat2 } from 'lucide-react'
|
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import Note from '../Note'
|
import Note from '../Note'
|
||||||
import Username from '../Username'
|
import RepostDescription from './RepostDescription'
|
||||||
|
|
||||||
export default function NormalNoteCard({
|
export default function MainNoteCard({
|
||||||
event,
|
event,
|
||||||
className,
|
className,
|
||||||
reposter,
|
reposter,
|
||||||
@@ -24,7 +21,6 @@ export default function NormalNoteCard({
|
|||||||
const { push } = useSecondaryPage()
|
const { push } = useSecondaryPage()
|
||||||
const { event: rootEvent } = useFetchEvent(getRootEventId(event))
|
const { event: rootEvent } = useFetchEvent(getRootEventId(event))
|
||||||
const { event: parentEvent } = useFetchEvent(getParentEventId(event))
|
const { event: parentEvent } = useFetchEvent(getParentEventId(event))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={className}
|
className={className}
|
||||||
@@ -48,22 +44,3 @@ export default function NormalNoteCard({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RepostDescription({
|
|
||||||
reposter,
|
|
||||||
className
|
|
||||||
}: {
|
|
||||||
reposter?: string | null
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
if (!reposter) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('flex gap-1 text-sm items-center text-muted-foreground mb-1', className)}>
|
|
||||||
<Repeat2 size={16} className="shrink-0" />
|
|
||||||
<Username userId={reposter} className="font-semibold truncate" skeletonClassName="h-3" />
|
|
||||||
<div>{t('reposted')}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
23
src/components/NoteCard/RepostDescription.tsx
Normal file
23
src/components/NoteCard/RepostDescription.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Repeat2 } from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Username from '../Username'
|
||||||
|
|
||||||
|
export default function RepostDescription({
|
||||||
|
reposter,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
reposter?: string | null
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
if (!reposter) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex gap-1 text-sm items-center text-muted-foreground mb-1', className)}>
|
||||||
|
<Repeat2 size={16} className="shrink-0" />
|
||||||
|
<Username userId={reposter} className="font-semibold truncate" skeletonClassName="h-3" />
|
||||||
|
<div>{t('reposted')}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { useMuteList } from '@/providers/MuteListProvider'
|
|||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import NormalNoteCard from './NormalNoteCard'
|
import GenericNoteCard from './GenericNoteCard'
|
||||||
|
|
||||||
export default function RepostNoteCard({
|
export default function RepostNoteCard({
|
||||||
event,
|
event,
|
||||||
@@ -17,7 +17,7 @@ export default function RepostNoteCard({
|
|||||||
const targetEvent = useMemo(() => {
|
const targetEvent = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
const targetEvent = event.content ? (JSON.parse(event.content) as Event) : null
|
const targetEvent = event.content ? (JSON.parse(event.content) as Event) : null
|
||||||
if (!targetEvent || !verifyEvent(targetEvent) || targetEvent.kind !== kinds.ShortTextNote) {
|
if (!targetEvent || !verifyEvent(targetEvent) || targetEvent.kind === kinds.Repost) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
client.addEventToCache(targetEvent)
|
client.addEventToCache(targetEvent)
|
||||||
@@ -38,5 +38,5 @@ export default function RepostNoteCard({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return <NormalNoteCard className={className} reposter={event.pubkey} event={targetEvent} />
|
return <GenericNoteCard className={className} reposter={event.pubkey} event={targetEvent} />
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/components/NoteCard/UnknownNoteCard.tsx
Normal file
65
src/components/NoteCard/UnknownNoteCard.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { getSharableEventId } from '@/lib/event'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Check, Copy } from 'lucide-react'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import RepostDescription from './RepostDescription'
|
||||||
|
import UserAvatar from '../UserAvatar'
|
||||||
|
import Username from '../Username'
|
||||||
|
import { FormattedTimestamp } from '../FormattedTimestamp'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export default function UnknownNoteCard({
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
embedded = false,
|
||||||
|
reposter
|
||||||
|
}: {
|
||||||
|
event: Event
|
||||||
|
className?: string
|
||||||
|
embedded?: boolean
|
||||||
|
reposter?: string
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className={cn(embedded ? 'p-2 sm:p-3 border rounded-lg' : 'px-4 py-3')}>
|
||||||
|
<RepostDescription reposter={reposter} />
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<UserAvatar userId={event.pubkey} size={embedded ? 'small' : 'normal'} />
|
||||||
|
<div
|
||||||
|
className={`flex-1 w-0 ${embedded ? 'flex space-x-2 items-center overflow-hidden' : ''}`}
|
||||||
|
>
|
||||||
|
<Username
|
||||||
|
userId={event.pubkey}
|
||||||
|
className={cn('font-semibold flex truncate', embedded ? 'text-sm' : '')}
|
||||||
|
skeletonClassName={embedded ? 'h-3' : 'h-4'}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground line-clamp-1">
|
||||||
|
<FormattedTimestamp timestamp={event.created_at} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 items-center text-muted-foreground font-medium mt-2">
|
||||||
|
<div>{t('Cannot handle event of kind k', { k: event.kind })}</div>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
navigator.clipboard.writeText(getSharableEventId(event))
|
||||||
|
setIsCopied(true)
|
||||||
|
setTimeout(() => setIsCopied(false), 2000)
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
{isCopied ? <Check /> : <Copy />} Copy event ID
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!embedded && <Separator />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMuteList } from '@/providers/MuteListProvider'
|
import { useMuteList } from '@/providers/MuteListProvider'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import NormalNoteCard from './NormalNoteCard'
|
import GenericNoteCard from './GenericNoteCard'
|
||||||
import RepostNoteCard from './RepostNoteCard'
|
import RepostNoteCard from './RepostNoteCard'
|
||||||
|
|
||||||
export default function NoteCard({
|
export default function NoteCard({
|
||||||
@@ -22,5 +22,5 @@ export default function NoteCard({
|
|||||||
<RepostNoteCard event={event} className={className} filterMutedNotes={filterMutedNotes} />
|
<RepostNoteCard event={event} className={className} filterMutedNotes={filterMutedNotes} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <NormalNoteCard event={event} className={className} />
|
return <GenericNoteCard event={event} className={className} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const SEARCHABLE_RELAY_URLS = ['wss://relay.nostr.band/', 'wss://search.n
|
|||||||
|
|
||||||
export const PICTURE_EVENT_KIND = 20
|
export const PICTURE_EVENT_KIND = 20
|
||||||
export const COMMENT_EVENT_KIND = 1111
|
export const COMMENT_EVENT_KIND = 1111
|
||||||
|
export const GROUP_METADATA_EVENT_KIND = 39000
|
||||||
|
|
||||||
export const URL_REGEX = /https?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_,:!~*]+/gu
|
export const URL_REGEX = /https?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_,:!~*]+/gu
|
||||||
export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||||
|
|||||||
@@ -178,6 +178,9 @@ export default {
|
|||||||
randomRelaysRefresh: 'Refresh',
|
randomRelaysRefresh: 'Refresh',
|
||||||
'Explore more': 'Explore more',
|
'Explore more': 'Explore more',
|
||||||
'Payment page': 'Payment page',
|
'Payment page': 'Payment page',
|
||||||
'Supported NIPs': 'Supported NIPs'
|
'Supported NIPs': 'Supported NIPs',
|
||||||
|
'Open in a': 'Open in {{a}}',
|
||||||
|
'Cannot handle event of kind k': 'Cannot handle event of kind {{k}}',
|
||||||
|
'Sorry! The note cannot be found 😔': 'Sorry! The note cannot be found 😔'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,9 @@ export default {
|
|||||||
randomRelaysRefresh: '换一批',
|
randomRelaysRefresh: '换一批',
|
||||||
'Explore more': '探索更多',
|
'Explore more': '探索更多',
|
||||||
'Payment page': '付款页面',
|
'Payment page': '付款页面',
|
||||||
'Supported NIPs': '支持的 NIP'
|
'Supported NIPs': '支持的 NIP',
|
||||||
|
'Open in a': '在 {{a}} 中打开',
|
||||||
|
'Cannot handle event of kind k': '无法处理类型为 {{k}} 的事件',
|
||||||
|
'Sorry! The note cannot be found 😔': '抱歉!找不到该笔记 😔'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export function isProtectedEvent(event: Event) {
|
|||||||
return event.tags.some(([tagName]) => tagName === '-')
|
return event.tags.some(([tagName]) => tagName === '-')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSupportedKind(kind: number) {
|
||||||
|
return [kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(kind)
|
||||||
|
}
|
||||||
|
|
||||||
export function getParentEventId(event?: Event) {
|
export function getParentEventId(event?: Event) {
|
||||||
if (!event || !isReplyNoteEvent(event)) return undefined
|
if (!event || !isReplyNoteEvent(event)) return undefined
|
||||||
const tag = event.tags.find(isReplyETag) ?? event.tags.find(tagNameEquals('e'))
|
const tag = event.tags.find(isReplyETag) ?? event.tags.find(tagNameEquals('e'))
|
||||||
@@ -81,7 +85,7 @@ export function getEventCoordinate(event: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSharableEventId(event: Event) {
|
export function getSharableEventId(event: Event) {
|
||||||
const hints = client.getEventHints(event.id).slice(0, 3)
|
const hints = client.getEventHints(event.id).slice(0, 2)
|
||||||
if (isReplaceable(event.kind)) {
|
if (isReplaceable(event.kind)) {
|
||||||
const identifier = event.tags.find(tagNameEquals('d'))?.[1] ?? ''
|
const identifier = event.tags.find(tagNameEquals('d'))?.[1] ?? ''
|
||||||
return nip19.naddrEncode({ pubkey: event.pubkey, kind: event.kind, identifier, relays: hints })
|
return nip19.naddrEncode({ pubkey: event.pubkey, kind: event.kind, identifier, relays: hints })
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import client from '@/services/client.service'
|
||||||
import { Event, nip19 } from 'nostr-tools'
|
import { Event, nip19 } from 'nostr-tools'
|
||||||
|
import { getSharableEventId } from './event'
|
||||||
|
|
||||||
export const toHome = () => '/'
|
export const toHome = () => '/'
|
||||||
export const toNote = (eventOrId: Pick<Event, 'id' | 'pubkey'> | string) => {
|
export const toNote = (eventOrId: Pick<Event, 'id' | 'pubkey'> | string) => {
|
||||||
if (typeof eventOrId === 'string') return `/notes/${eventOrId}`
|
if (typeof eventOrId === 'string') return `/notes/${eventOrId}`
|
||||||
const nevent = nip19.neventEncode({ id: eventOrId.id, author: eventOrId.pubkey })
|
const relay = client.getEventHint(eventOrId.id)
|
||||||
|
const nevent = nip19.neventEncode({ id: eventOrId.id, author: eventOrId.pubkey, relays: [relay] })
|
||||||
return `/notes/${nevent}`
|
return `/notes/${nevent}`
|
||||||
}
|
}
|
||||||
export const toNoteList = ({ hashtag, search }: { hashtag?: string; search?: string }) => {
|
export const toNoteList = ({ hashtag, search }: { hashtag?: string; search?: string }) => {
|
||||||
@@ -40,7 +43,10 @@ export const toProfileEditor = () => '/profile-editor'
|
|||||||
export const toRelay = (url: string) => `/relays/${encodeURIComponent(url)}`
|
export const toRelay = (url: string) => `/relays/${encodeURIComponent(url)}`
|
||||||
export const toMuteList = () => '/mutes'
|
export const toMuteList = () => '/mutes'
|
||||||
|
|
||||||
export const toNoStrudelProfile = (id: string) => `https://nostrudel.ninja/#/u/${id}`
|
export const toHablaLongFormArticle = (event: Event) => {
|
||||||
export const toNoStrudelNote = (id: string) => `https://nostrudel.ninja/#/n/${id}`
|
return `https://habla.news/a/${getSharableEventId(event)}`
|
||||||
export const toNoStrudelArticle = (id: string) => `https://nostrudel.ninja/#/articles/${id}`
|
}
|
||||||
export const toNoStrudelStream = (id: string) => `https://nostrudel.ninja/#/streams/${id}`
|
export const toZapStreamLiveEvent = (event: Event) => {
|
||||||
|
return `https://zap.stream/${getSharableEventId(event)}`
|
||||||
|
}
|
||||||
|
export const toChachiChat = (relay: string, d: string) => `https://chachi.chat/${relay}/${d}`
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useFetchEvent } from '@/hooks'
|
|||||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||||
import { getParentEventId, getRootEventId, isPictureEvent } from '@/lib/event'
|
import { getParentEventId, getRootEventId, isPictureEvent } from '@/lib/event'
|
||||||
import { toNote } from '@/lib/link'
|
import { toNote } from '@/lib/link'
|
||||||
|
import { kinds } from 'nostr-tools'
|
||||||
import { forwardRef, useMemo } from 'react'
|
import { forwardRef, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import NotFoundPage from '../NotFoundPage'
|
import NotFoundPage from '../NotFoundPage'
|
||||||
@@ -57,15 +58,15 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
|
|||||||
<Note key={`note-${event.id}`} event={event} fetchNoteStats />
|
<Note key={`note-${event.id}`} event={event} fetchNoteStats />
|
||||||
</div>
|
</div>
|
||||||
<Separator className="mb-2 mt-4" />
|
<Separator className="mb-2 mt-4" />
|
||||||
{isPictureEvent(event) ? (
|
{event.kind === kinds.ShortTextNote ? (
|
||||||
|
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} className="px-2" />
|
||||||
|
) : isPictureEvent(event) ? (
|
||||||
<Nip22ReplyNoteList
|
<Nip22ReplyNoteList
|
||||||
key={`nip22-reply-note-list-${event.id}`}
|
key={`nip22-reply-note-list-${event.id}`}
|
||||||
event={event}
|
event={event}
|
||||||
className="px-2"
|
className="px-2"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : null}
|
||||||
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} className="px-2" />
|
|
||||||
)}
|
|
||||||
</SecondaryPageLayout>
|
</SecondaryPageLayout>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user