feat: poll response notification

This commit is contained in:
codytseng 2025-07-28 22:33:51 +08:00
parent 20c712c56b
commit bcd149b304
14 changed files with 209 additions and 93 deletions

View file

@ -6,18 +6,16 @@ import { useTranslation } from 'react-i18next'
export default function CommunityDefinitionPreview({
event,
className,
onClick
className
}: {
event: Event
className?: string
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
}) {
const { t } = useTranslation()
const metadata = useMemo(() => getCommunityDefinitionFromEvent(event), [event])
return (
<div className={cn('pointer-events-none', className)} onClick={onClick}>
<div className={cn('pointer-events-none', className)}>
[{t('Community')}] <span className="italic pr-0.5">{metadata.name}</span>
</div>
)

View file

@ -0,0 +1,63 @@
import {
EmbeddedEmojiParser,
EmbeddedEventParser,
EmbeddedImageParser,
EmbeddedMentionParser,
EmbeddedVideoParser,
parseContent
} from '@/lib/content-parser'
import { cn } from '@/lib/utils'
import { TEmoji } from '@/types'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { EmbeddedMentionText } from '../Embedded'
import Emoji from '../Emoji'
export default function Content({
content,
className,
emojiInfos
}: {
content: string
className?: string
emojiInfos?: TEmoji[]
}) {
const { t } = useTranslation()
const nodes = useMemo(() => {
return parseContent(content, [
EmbeddedImageParser,
EmbeddedVideoParser,
EmbeddedEventParser,
EmbeddedMentionParser,
EmbeddedEmojiParser
])
}, [content])
return (
<span className={cn('pointer-events-none', className)}>
{nodes.map((node, index) => {
if (node.type === 'text') {
return node.data
}
if (node.type === 'image' || node.type === 'images') {
return index > 0 ? ` [${t('image')}]` : `[${t('image')}]`
}
if (node.type === 'video') {
return index > 0 ? ` [${t('video')}]` : `[${t('video')}]`
}
if (node.type === 'event') {
return index > 0 ? ` [${t('note')}]` : `[${t('note')}]`
}
if (node.type === 'mention') {
return <EmbeddedMentionText key={index} userId={node.data.split(':')[1]} />
}
if (node.type === 'emoji') {
const shortcode = node.data.split(':')[1]
const emoji = emojiInfos?.find((e) => e.shortcode === shortcode)
if (!emoji) return node.data
return <Emoji key={index} emoji={emoji} />
}
})}
</span>
)
}

View file

@ -6,18 +6,16 @@ import { useTranslation } from 'react-i18next'
export default function GroupMetadataPreview({
event,
className,
onClick
className
}: {
event: Event
className?: string
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
}) {
const { t } = useTranslation()
const metadata = useMemo(() => getGroupMetadataFromEvent(event), [event])
return (
<div className={cn('pointer-events-none', className)} onClick={onClick}>
<div className={cn('pointer-events-none', className)}>
[{t('Group')}] <span className="italic pr-0.5">{metadata.name}</span>
</div>
)

View file

@ -0,0 +1,30 @@
import { useTranslatedEvent } from '@/hooks'
import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Content from './Content'
export default function HighlightPreview({
event,
className
}: {
event: Event
className?: string
}) {
const { t } = useTranslation()
const translatedEvent = useTranslatedEvent(event.id)
const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event.tags), [event])
return (
<div className={cn('pointer-events-none', className)}>
[{t('Highlight')}]{' '}
<Content
content={translatedEvent?.content ?? event.content}
emojiInfos={emojiInfos}
className="italic pr-0.5"
/>
</div>
)
}

View file

@ -6,18 +6,16 @@ import { useTranslation } from 'react-i18next'
export default function LiveEventPreview({
event,
className,
onClick
className
}: {
event: Event
className?: string
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
}) {
const { t } = useTranslation()
const metadata = useMemo(() => getLiveEventMetadataFromEvent(event), [event])
return (
<div className={cn('pointer-events-none', className)} onClick={onClick}>
<div className={cn('pointer-events-none', className)}>
[{t('Live event')}] <span className="italic pr-0.5">{metadata.title}</span>
</div>
)

View file

@ -6,18 +6,16 @@ import { useTranslation } from 'react-i18next'
export default function LongFormArticlePreview({
event,
className,
onClick
className
}: {
event: Event
className?: string
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
}) {
const { t } = useTranslation()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
return (
<div className={cn('pointer-events-none', className)} onClick={onClick}>
<div className={cn('pointer-events-none', className)}>
[{t('Article')}] <span className="italic pr-0.5">{metadata.title}</span>
</div>
)

View file

@ -1,68 +1,24 @@
import { useTranslatedEvent } from '@/hooks'
import {
EmbeddedEmojiParser,
EmbeddedEventParser,
EmbeddedImageParser,
EmbeddedMentionParser,
EmbeddedVideoParser,
parseContent
} from '@/lib/content-parser'
import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { EmbeddedMentionText } from '../Embedded'
import Emoji from '../Emoji'
import Content from './Content'
export default function NormalContentPreview({
event,
className,
onClick
className
}: {
event: Event
className?: string
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
}) {
const { t } = useTranslation()
const translatedEvent = useTranslatedEvent(event?.id)
const nodes = useMemo(() => {
return parseContent(event.content, [
EmbeddedImageParser,
EmbeddedVideoParser,
EmbeddedEventParser,
EmbeddedMentionParser,
EmbeddedEmojiParser
])
}, [event, translatedEvent])
const emojiInfos = getEmojiInfosFromEmojiTags(event?.tags)
const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event?.tags), [event])
return (
<div className={cn('pointer-events-none', className)} onClick={onClick}>
{nodes.map((node, index) => {
if (node.type === 'text') {
return node.data
}
if (node.type === 'image' || node.type === 'images') {
return index > 0 ? ` [${t('image')}]` : `[${t('image')}]`
}
if (node.type === 'video') {
return index > 0 ? ` [${t('video')}]` : `[${t('video')}]`
}
if (node.type === 'event') {
return index > 0 ? ` [${t('note')}]` : `[${t('note')}]`
}
if (node.type === 'mention') {
return <EmbeddedMentionText key={index} userId={node.data.split(':')[1]} />
}
if (node.type === 'emoji') {
const shortcode = node.data.split(':')[1]
const emoji = emojiInfos.find((e) => e.shortcode === shortcode)
if (!emoji) return node.data
return <Emoji key={index} emoji={emoji} />
}
})}
</div>
<Content
content={translatedEvent?.content ?? event.content}
className={className}
emojiInfos={emojiInfos}
/>
)
}

View file

@ -0,0 +1,24 @@
import { useTranslatedEvent } from '@/hooks'
import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Content from './Content'
export default function PollPreview({ event, className }: { event: Event; className?: string }) {
const { t } = useTranslation()
const translatedEvent = useTranslatedEvent(event.id)
const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event.tags), [event])
return (
<div className={cn('pointer-events-none', className)}>
[{t('Poll')}]{' '}
<Content
content={translatedEvent?.content ?? event.content}
emojiInfos={emojiInfos}
className="italic pr-0.5"
/>
</div>
)
}

View file

@ -6,18 +6,18 @@ import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import CommunityDefinitionPreview from './CommunityDefinitionPreview'
import GroupMetadataPreview from './GroupMetadataPreview'
import HighlightPreview from './HighlightPreview'
import LiveEventPreview from './LiveEventPreview'
import LongFormArticlePreview from './LongFormArticlePreview'
import NormalContentPreview from './NormalContentPreview'
import PollPreview from './PollPreview'
export default function ContentPreview({
event,
className,
onClick
className
}: {
event?: Event
className?: string
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
}) {
const { t } = useTranslation()
const { mutePubkeys } = useMuteList()
@ -37,39 +37,31 @@ export default function ContentPreview({
}
if ([kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.PICTURE].includes(event.kind)) {
return <NormalContentPreview event={event} className={className} onClick={onClick} />
return <NormalContentPreview event={event} className={className} />
}
if (event.kind === kinds.Highlights) {
return (
<div className={cn('pointer-events-none', className)}>
[{t('Highlight')}] <span className="italic pr-0.5">{event.content}</span>
</div>
)
return <HighlightPreview event={event} className={className} />
}
if (event.kind === ExtendedKind.POLL) {
return (
<div className={cn('pointer-events-none', className)}>
[{t('Poll')}] <span className="italic pr-0.5">{event.content}</span>
</div>
)
return <PollPreview event={event} className={className} />
}
if (event.kind === kinds.LongFormArticle) {
return <LongFormArticlePreview event={event} className={className} onClick={onClick} />
return <LongFormArticlePreview event={event} className={className} />
}
if (event.kind === ExtendedKind.GROUP_METADATA) {
return <GroupMetadataPreview event={event} className={className} onClick={onClick} />
return <GroupMetadataPreview event={event} className={className} />
}
if (event.kind === kinds.CommunityDefinition) {
return <CommunityDefinitionPreview event={event} className={className} onClick={onClick} />
return <CommunityDefinitionPreview event={event} className={className} />
}
if (event.kind === kinds.LiveEvent) {
return <LiveEventPreview event={event} className={className} onClick={onClick} />
return <LiveEventPreview event={event} className={className} />
}
return <div className={className}>[{t('Cannot handle event of kind k', { k: event.kind })}]</div>