From bcd149b30406f8edd0a34b76485ba3fbeab3922d Mon Sep 17 00:00:00 2001 From: codytseng Date: Mon, 28 Jul 2025 22:33:51 +0800 Subject: [PATCH] feat: poll response notification --- .../CommunityDefinitionPreview.tsx | 6 +- src/components/ContentPreview/Content.tsx | 63 +++++++++++++++++++ .../ContentPreview/GroupMetadataPreview.tsx | 6 +- .../ContentPreview/HighlightPreview.tsx | 30 +++++++++ .../ContentPreview/LiveEventPreview.tsx | 6 +- .../ContentPreview/LongFormArticlePreview.tsx | 6 +- .../ContentPreview/NormalContentPreview.tsx | 60 +++--------------- src/components/ContentPreview/PollPreview.tsx | 24 +++++++ src/components/ContentPreview/index.tsx | 28 +++------ src/components/Note/Highlight.tsx | 7 ++- .../PollResponseNotification.tsx | 47 ++++++++++++++ .../NotificationItem/ReactionNotification.tsx | 4 +- .../NotificationItem/index.tsx | 4 ++ src/components/NotificationList/index.tsx | 11 +++- 14 files changed, 209 insertions(+), 93 deletions(-) create mode 100644 src/components/ContentPreview/Content.tsx create mode 100644 src/components/ContentPreview/HighlightPreview.tsx create mode 100644 src/components/ContentPreview/PollPreview.tsx create mode 100644 src/components/NotificationList/NotificationItem/PollResponseNotification.tsx diff --git a/src/components/ContentPreview/CommunityDefinitionPreview.tsx b/src/components/ContentPreview/CommunityDefinitionPreview.tsx index 0f58db70..82d7e70e 100644 --- a/src/components/ContentPreview/CommunityDefinitionPreview.tsx +++ b/src/components/ContentPreview/CommunityDefinitionPreview.tsx @@ -6,18 +6,16 @@ import { useTranslation } from 'react-i18next' export default function CommunityDefinitionPreview({ event, - className, - onClick + className }: { event: Event className?: string - onClick?: React.MouseEventHandler | undefined }) { const { t } = useTranslation() const metadata = useMemo(() => getCommunityDefinitionFromEvent(event), [event]) return ( -
+
[{t('Community')}] {metadata.name}
) diff --git a/src/components/ContentPreview/Content.tsx b/src/components/ContentPreview/Content.tsx new file mode 100644 index 00000000..34832855 --- /dev/null +++ b/src/components/ContentPreview/Content.tsx @@ -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 ( + + {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 + } + if (node.type === 'emoji') { + const shortcode = node.data.split(':')[1] + const emoji = emojiInfos?.find((e) => e.shortcode === shortcode) + if (!emoji) return node.data + return + } + })} + + ) +} diff --git a/src/components/ContentPreview/GroupMetadataPreview.tsx b/src/components/ContentPreview/GroupMetadataPreview.tsx index 842bb1db..531e3768 100644 --- a/src/components/ContentPreview/GroupMetadataPreview.tsx +++ b/src/components/ContentPreview/GroupMetadataPreview.tsx @@ -6,18 +6,16 @@ import { useTranslation } from 'react-i18next' export default function GroupMetadataPreview({ event, - className, - onClick + className }: { event: Event className?: string - onClick?: React.MouseEventHandler | undefined }) { const { t } = useTranslation() const metadata = useMemo(() => getGroupMetadataFromEvent(event), [event]) return ( -
+
[{t('Group')}] {metadata.name}
) diff --git a/src/components/ContentPreview/HighlightPreview.tsx b/src/components/ContentPreview/HighlightPreview.tsx new file mode 100644 index 00000000..b1656831 --- /dev/null +++ b/src/components/ContentPreview/HighlightPreview.tsx @@ -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 ( +
+ [{t('Highlight')}]{' '} + +
+ ) +} diff --git a/src/components/ContentPreview/LiveEventPreview.tsx b/src/components/ContentPreview/LiveEventPreview.tsx index 1239379f..7de7324c 100644 --- a/src/components/ContentPreview/LiveEventPreview.tsx +++ b/src/components/ContentPreview/LiveEventPreview.tsx @@ -6,18 +6,16 @@ import { useTranslation } from 'react-i18next' export default function LiveEventPreview({ event, - className, - onClick + className }: { event: Event className?: string - onClick?: React.MouseEventHandler | undefined }) { const { t } = useTranslation() const metadata = useMemo(() => getLiveEventMetadataFromEvent(event), [event]) return ( -
+
[{t('Live event')}] {metadata.title}
) diff --git a/src/components/ContentPreview/LongFormArticlePreview.tsx b/src/components/ContentPreview/LongFormArticlePreview.tsx index 73d92b6a..c42cb965 100644 --- a/src/components/ContentPreview/LongFormArticlePreview.tsx +++ b/src/components/ContentPreview/LongFormArticlePreview.tsx @@ -6,18 +6,16 @@ import { useTranslation } from 'react-i18next' export default function LongFormArticlePreview({ event, - className, - onClick + className }: { event: Event className?: string - onClick?: React.MouseEventHandler | undefined }) { const { t } = useTranslation() const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) return ( -
+
[{t('Article')}] {metadata.title}
) diff --git a/src/components/ContentPreview/NormalContentPreview.tsx b/src/components/ContentPreview/NormalContentPreview.tsx index a4569cfd..55456896 100644 --- a/src/components/ContentPreview/NormalContentPreview.tsx +++ b/src/components/ContentPreview/NormalContentPreview.tsx @@ -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 | 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 ( -
- {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 - } - if (node.type === 'emoji') { - const shortcode = node.data.split(':')[1] - const emoji = emojiInfos.find((e) => e.shortcode === shortcode) - if (!emoji) return node.data - return - } - })} -
+ ) } diff --git a/src/components/ContentPreview/PollPreview.tsx b/src/components/ContentPreview/PollPreview.tsx new file mode 100644 index 00000000..77e58d65 --- /dev/null +++ b/src/components/ContentPreview/PollPreview.tsx @@ -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 ( +
+ [{t('Poll')}]{' '} + +
+ ) +} diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx index a02ec279..ea668fd2 100644 --- a/src/components/ContentPreview/index.tsx +++ b/src/components/ContentPreview/index.tsx @@ -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 | 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 + return } if (event.kind === kinds.Highlights) { - return ( -
- [{t('Highlight')}] {event.content} -
- ) + return } if (event.kind === ExtendedKind.POLL) { - return ( -
- [{t('Poll')}] {event.content} -
- ) + return } if (event.kind === kinds.LongFormArticle) { - return + return } if (event.kind === ExtendedKind.GROUP_METADATA) { - return + return } if (event.kind === kinds.CommunityDefinition) { - return + return } if (event.kind === kinds.LiveEvent) { - return + return } return
[{t('Cannot handle event of kind k', { k: event.kind })}]
diff --git a/src/components/Note/Highlight.tsx b/src/components/Note/Highlight.tsx index 7bc5eded..11b3f7e5 100644 --- a/src/components/Note/Highlight.tsx +++ b/src/components/Note/Highlight.tsx @@ -111,14 +111,15 @@ function HighlightSource({ event }: { event: Event }) {
{t('From')}
{pubkey && } {referenceEvent ? ( - { e.stopPropagation() push(toNote(referenceEvent)) }} - /> + > + +
) : referenceEventId ? (
{ + const eTag = notification.tags.find(tagNameEquals('e')) + return eTag ? generateBech32IdFromETag(eTag) : undefined + }, [notification]) + const { event: pollEvent } = useFetchEvent(eventId) + + if (!pollEvent) { + return null + } + + return ( +
push(toNote(pollEvent))} + > + + + +
+ +
+
+ ) +} diff --git a/src/components/NotificationList/NotificationItem/ReactionNotification.tsx b/src/components/NotificationList/NotificationItem/ReactionNotification.tsx index 90b11b2a..b6164d3f 100644 --- a/src/components/NotificationList/NotificationItem/ReactionNotification.tsx +++ b/src/components/NotificationList/NotificationItem/ReactionNotification.tsx @@ -1,7 +1,7 @@ import Image from '@/components/Image' import { useFetchEvent } from '@/hooks' import { toNote } from '@/lib/link' -import { tagNameEquals } from '@/lib/tag' +import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag' import { cn } from '@/lib/utils' import { useSecondaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' @@ -26,7 +26,7 @@ export function ReactionNotification({ if (targetPubkey !== pubkey) return undefined const eTag = notification.tags.findLast(tagNameEquals('e')) - return eTag?.[1] + return eTag ? generateBech32IdFromETag(eTag) : undefined }, [notification, pubkey]) const { event } = useFetchEvent(eventId) const reaction = useMemo(() => { diff --git a/src/components/NotificationList/NotificationItem/index.tsx b/src/components/NotificationList/NotificationItem/index.tsx index cd009425..6ef697d9 100644 --- a/src/components/NotificationList/NotificationItem/index.tsx +++ b/src/components/NotificationList/NotificationItem/index.tsx @@ -2,6 +2,7 @@ import { ExtendedKind } from '@/constants' import { useMuteList } from '@/providers/MuteListProvider' import { Event, kinds } from 'nostr-tools' import { CommentNotification } from './CommentNotification' +import { PollResponseNotification } from './PollResponseNotification' import { ReactionNotification } from './ReactionNotification' import { ReplyNotification } from './ReplyNotification' import { RepostNotification } from './RepostNotification' @@ -33,5 +34,8 @@ export function NotificationItem({ if (notification.kind === ExtendedKind.COMMENT) { return } + if (notification.kind === ExtendedKind.POLL_RESPONSE) { + return + } return null } diff --git a/src/components/NotificationList/index.tsx b/src/components/NotificationList/index.tsx index 151e31fe..c5a3dacc 100644 --- a/src/components/NotificationList/index.tsx +++ b/src/components/NotificationList/index.tsx @@ -41,11 +41,18 @@ const NotificationList = forwardRef((_, ref) => { case 'mentions': return [kinds.ShortTextNote, ExtendedKind.COMMENT] case 'reactions': - return [kinds.Reaction, kinds.Repost] + return [kinds.Reaction, kinds.Repost, ExtendedKind.POLL_RESPONSE] case 'zaps': return [kinds.Zap] default: - return [kinds.ShortTextNote, kinds.Repost, kinds.Reaction, kinds.Zap, ExtendedKind.COMMENT] + return [ + kinds.ShortTextNote, + kinds.Repost, + kinds.Reaction, + kinds.Zap, + ExtendedKind.COMMENT, + ExtendedKind.POLL_RESPONSE + ] } }, [notificationType]) useImperativeHandle(