refactor: 💨

This commit is contained in:
codytseng
2025-07-07 22:34:59 +08:00
parent c729c20771
commit 8c5cc1041b
46 changed files with 1008 additions and 879 deletions

View File

@@ -0,0 +1,42 @@
import { getCommunityDefinition } from '@/lib/event'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import ClientSelect from '../ClientSelect'
import Image from '../Image'
export default function CommunityDefinition({
event,
className
}: {
event: Event
className?: string
}) {
const metadata = useMemo(() => getCommunityDefinition(event), [event])
const communityNameComponent = (
<div className="text-xl font-semibold line-clamp-1">{metadata.name}</div>
)
const communityDescriptionComponent = metadata.description && (
<div className="text-sm text-muted-foreground line-clamp-2">{metadata.description}</div>
)
return (
<div className={className}>
<div className="flex gap-4">
{metadata.image && (
<Image
image={{ url: metadata.image }}
className="rounded-lg aspect-square object-cover bg-foreground h-20"
hideIfError
/>
)}
<div className="flex-1 w-0 space-y-1">
{communityNameComponent}
{communityDescriptionComponent}
</div>
</div>
<ClientSelect variant="secondary" className="w-full mt-2" event={event} />
</div>
)
}

View File

@@ -0,0 +1,49 @@
import { getGroupMetadata } from '@/lib/event'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import ClientSelect from '../ClientSelect'
import Image from '../Image'
export default function GroupMetadata({
event,
originalNoteId,
className
}: {
event: Event
originalNoteId?: string
className?: string
}) {
const metadata = useMemo(() => getGroupMetadata(event), [event])
const groupNameComponent = (
<div className="text-xl font-semibold line-clamp-1">{metadata.name}</div>
)
const groupAboutComponent = metadata.about && (
<div className="text-sm text-muted-foreground line-clamp-2">{metadata.about}</div>
)
return (
<div className={className}>
<div className="flex gap-4">
{metadata.picture && (
<Image
image={{ url: metadata.picture }}
className="rounded-lg aspect-square object-cover bg-foreground h-20"
hideIfError
/>
)}
<div className="flex-1 w-0 space-y-1">
{groupNameComponent}
{groupAboutComponent}
</div>
</div>
<ClientSelect
variant="secondary"
className="w-full mt-2"
event={event}
originalNoteId={originalNoteId}
/>
</div>
)
}

View File

@@ -1,5 +1,5 @@
import { useFetchEvent, useTranslatedEvent } from '@/hooks'
import { createFakeEvent, isSupportedKind } from '@/lib/event'
import { createFakeEvent } from '@/lib/event'
import { toNjump, toNote } from '@/lib/link'
import { isValidPubkey } from '@/lib/pubkey'
import { generateEventIdFromATag } from '@/lib/tag'
@@ -110,7 +110,7 @@ function HighlightSource({ event }: { event: Event }) {
<div className="flex items-center gap-2 text-muted-foreground">
<div className="shrink-0">{t('From')}</div>
{pubkey && <UserAvatar userId={pubkey} size="xSmall" className="cursor-pointer" />}
{referenceEvent && isSupportedKind(referenceEvent.kind) ? (
{referenceEvent ? (
<ContentPreview
className="truncate underline pointer-events-auto cursor-pointer hover:text-foreground"
event={referenceEvent}

View File

@@ -0,0 +1,80 @@
import { Badge } from '@/components/ui/badge'
import { getLiveEventMetadata } from '@/lib/event'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import ClientSelect from '../ClientSelect'
import Image from '../Image'
export default function LiveEvent({ event, className }: { event: Event; className?: string }) {
const { isSmallScreen } = useScreenSize()
const metadata = useMemo(() => getLiveEventMetadata(event), [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 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>
)
if (isSmallScreen) {
return (
<div className={className}>
{metadata.image && (
<Image
image={{ url: metadata.image }}
className="w-full aspect-video object-cover rounded-lg"
hideIfError
/>
)}
<div className="space-y-1">
{titleComponent}
{liveStatusComponent}
{summaryComponent}
{tagsComponent}
<ClientSelect variant="secondary" className="w-full mt-2" event={event} />
</div>
</div>
)
}
return (
<div className={className}>
<div className="flex gap-4">
{metadata.image && (
<Image
image={{ url: metadata.image }}
className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44"
hideIfError
/>
)}
<div className="flex-1 w-0 space-y-1">
{titleComponent}
{liveStatusComponent}
{summaryComponent}
{tagsComponent}
</div>
</div>
<ClientSelect variant="secondary" className="w-full mt-2" event={event} />
</div>
)
}

View File

@@ -0,0 +1,74 @@
import { Badge } from '@/components/ui/badge'
import { getLongFormArticleMetadata } from '@/lib/event'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import ClientSelect from '../ClientSelect'
import Image from '../Image'
export default function LongFormArticle({
event,
className
}: {
event: Event
className?: string
}) {
const { isSmallScreen } = useScreenSize()
const metadata = useMemo(() => getLongFormArticleMetadata(event), [event])
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>
)
if (isSmallScreen) {
return (
<div className={className}>
{metadata.image && (
<Image
image={{ url: metadata.image }}
className="w-full aspect-video object-cover rounded-lg"
hideIfError
/>
)}
<div className="space-y-1">
{titleComponent}
{summaryComponent}
{tagsComponent}
<ClientSelect variant="secondary" className="w-full mt-2" event={event} />
</div>
</div>
)
}
return (
<div className={className}>
<div className="flex gap-4">
{metadata.image && (
<Image
image={{ url: metadata.image }}
className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44"
hideIfError
/>
)}
<div className="flex-1 w-0 space-y-1">
{titleComponent}
{summaryComponent}
{tagsComponent}
</div>
</div>
<ClientSelect variant="secondary" className="w-full mt-2" event={event} />
</div>
)
}

View File

@@ -0,0 +1,23 @@
import { Button } from '@/components/ui/button'
import { Eye } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export default function MutedNote({ show }: { show: () => void }) {
const { t } = useTranslation()
return (
<div className="flex flex-col gap-2 items-center text-muted-foreground font-medium my-4">
<div>{t('This user has been muted')}</div>
<Button
onClick={(e) => {
e.stopPropagation()
show()
}}
variant="outline"
>
<Eye />
{t('Temporarily display this note')}
</Button>
</div>
)
}

View File

@@ -1,10 +1,7 @@
import { Button } from '@/components/ui/button'
import { getSharableEventId } from '@/lib/event'
import { toNjump } from '@/lib/link'
import { cn } from '@/lib/utils'
import { ExternalLink } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next'
import ClientSelect from '../ClientSelect'
export function UnknownNote({ event, className }: { event: Event; className?: string }) {
const { t } = useTranslation()
@@ -17,16 +14,7 @@ export function UnknownNote({ event, className }: { event: Event; className?: st
)}
>
<div>{t('Cannot handle event of kind k', { k: event.kind })}</div>
<Button
onClick={(e) => {
e.stopPropagation()
window.open(toNjump(getSharableEventId(event)), '_blank')
}}
variant="outline"
>
<ExternalLink />
<div>{t('View on njump.me')}</div>
</Button>
<ClientSelect event={event} variant="secondary" />
</div>
)
}

View File

@@ -1,13 +1,14 @@
import { useSecondaryPage } from '@/PageManager'
import { ExtendedKind } from '@/constants'
import {
extractImageInfosFromEventTags,
getParentEventId,
getUsingClient,
isNsfwEvent,
isPictureEvent,
isSupportedKind
isPictureEvent
} from '@/lib/event'
import { toNote } from '@/lib/link'
import { useMuteList } from '@/providers/MuteListProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event, kinds } from 'nostr-tools'
import { useMemo, useState } from 'react'
@@ -20,18 +21,25 @@ import ParentNotePreview from '../ParentNotePreview'
import TranslateButton from '../TranslateButton'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import CommunityDefinition from './CommunityDefinition'
import GroupMetadata from './GroupMetadata'
import Highlight from './Highlight'
import IValue from './IValue'
import LiveEvent from './LiveEvent'
import LongFormArticle from './LongFormArticle'
import MutedNote from './MutedNote'
import NsfwNote from './NsfwNote'
import { UnknownNote } from './UnknownNote'
export default function Note({
event,
originalNoteId,
size = 'normal',
className,
hideParentNotePreview = false
}: {
event: Event
originalNoteId?: string
size?: 'normal' | 'small'
className?: string
hideParentNotePreview?: boolean
@@ -48,14 +56,37 @@ export default function Note({
)
const usingClient = useMemo(() => getUsingClient(event), [event])
const [showNsfw, setShowNsfw] = useState(false)
const { mutePubkeys } = useMuteList()
const [showMuted, setShowMuted] = useState(false)
let content: React.ReactNode
if (!isSupportedKind(event.kind)) {
if (
![
kinds.ShortTextNote,
kinds.Highlights,
kinds.LongFormArticle,
kinds.LiveEvent,
kinds.CommunityDefinition,
ExtendedKind.GROUP_METADATA,
ExtendedKind.PICTURE,
ExtendedKind.COMMENT
].includes(event.kind)
) {
content = <UnknownNote className="mt-2" event={event} />
} else if (mutePubkeys.includes(event.pubkey) && !showMuted) {
content = <MutedNote show={() => setShowMuted(true)} />
} else if (isNsfwEvent(event) && !showNsfw) {
content = <NsfwNote show={() => setShowNsfw(true)} />
} else if (event.kind === kinds.Highlights) {
content = <Highlight className="mt-2" event={event} />
} else if (event.kind === kinds.LongFormArticle) {
content = <LongFormArticle className="mt-2" event={event} />
} else if (event.kind === kinds.LiveEvent) {
content = <LiveEvent className="mt-2" event={event} />
} else if (event.kind === ExtendedKind.GROUP_METADATA) {
content = <GroupMetadata className="mt-2" event={event} originalNoteId={originalNoteId} />
} else if (event.kind === kinds.CommunityDefinition) {
content = <CommunityDefinition className="mt-2" event={event} />
} else {
content = <Content className="mt-2" event={event} />
}