Browse Source

feat: add more note interactions lists (#467)

Co-authored-by: Trevor Arjeski <tmarjeski@gmail.com>
imwald
Cody Tseng 7 months ago committed by GitHub
parent
commit
f2c87b8d5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 197
      src/components/Content/index.tsx
  2. 19
      src/components/Emoji/index.tsx
  3. 5
      src/components/NoteInteractions/Tabs.tsx
  4. 42
      src/components/NoteInteractions/index.tsx
  5. 4
      src/components/NoteStats/TopZaps.tsx
  6. 7
      src/components/QuoteList/index.tsx
  7. 89
      src/components/ReactionList/index.tsx
  8. 2
      src/components/ReplyNoteList/index.tsx
  9. 81
      src/components/RepostList/index.tsx
  10. 84
      src/components/ZapList/index.tsx
  11. 9
      src/i18n/locales/ar.ts
  12. 9
      src/i18n/locales/de.ts
  13. 9
      src/i18n/locales/en.ts
  14. 9
      src/i18n/locales/es.ts
  15. 9
      src/i18n/locales/fa.ts
  16. 9
      src/i18n/locales/fr.ts
  17. 9
      src/i18n/locales/it.ts
  18. 9
      src/i18n/locales/ja.ts
  19. 9
      src/i18n/locales/ko.ts
  20. 9
      src/i18n/locales/pl.ts
  21. 9
      src/i18n/locales/pt-BR.ts
  22. 9
      src/i18n/locales/pt-PT.ts
  23. 9
      src/i18n/locales/ru.ts
  24. 9
      src/i18n/locales/th.ts
  25. 9
      src/i18n/locales/zh.ts
  26. 78
      src/lib/draft-event.ts
  27. 67
      src/services/note-stats.service.ts

197
src/components/Content/index.tsx

@ -33,103 +33,114 @@ import MediaPlayer from '../MediaPlayer' @@ -33,103 +33,114 @@ import MediaPlayer from '../MediaPlayer'
import WebPreview from '../WebPreview'
import YoutubeEmbeddedPlayer from '../YoutubeEmbeddedPlayer'
const Content = memo(({ event, className }: { event: Event; className?: string }) => {
const translatedEvent = useTranslatedEvent(event.id)
const nodes = parseContent(translatedEvent?.content ?? event.content, [
EmbeddedYoutubeParser,
EmbeddedImageParser,
EmbeddedMediaParser,
EmbeddedNormalUrlParser,
EmbeddedLNInvoiceParser,
EmbeddedWebsocketUrlParser,
EmbeddedEventParser,
EmbeddedMentionParser,
EmbeddedHashtagParser,
EmbeddedEmojiParser
])
const Content = memo(
({ event, content, className }: { event?: Event; content?: string; className?: string }) => {
const translatedEvent = useTranslatedEvent(event?.id)
const _content = translatedEvent?.content ?? event?.content ?? content
if (!_content) return null
const imageInfos = getImageInfosFromEvent(event)
const allImages = nodes
.map((node) => {
if (node.type === 'image') {
const imageInfo = imageInfos.find((image) => image.url === node.data)
if (imageInfo) {
return imageInfo
}
const tag = mediaUpload.getImetaTagByUrl(node.data)
return tag
? getImageInfoFromImetaTag(tag, event.pubkey)
: { url: node.data, pubkey: event.pubkey }
}
if (node.type === 'images') {
const urls = Array.isArray(node.data) ? node.data : [node.data]
return urls.map((url) => {
const imageInfo = imageInfos.find((image) => image.url === url)
return imageInfo ?? { url, pubkey: event.pubkey }
})
}
return null
})
.filter(Boolean)
.flat() as TImageInfo[]
let imageIndex = 0
const emojiInfos = getEmojiInfosFromEmojiTags(event.tags)
const nodes = parseContent(_content, [
EmbeddedYoutubeParser,
EmbeddedImageParser,
EmbeddedMediaParser,
EmbeddedNormalUrlParser,
EmbeddedLNInvoiceParser,
EmbeddedWebsocketUrlParser,
EmbeddedEventParser,
EmbeddedMentionParser,
EmbeddedHashtagParser,
EmbeddedEmojiParser
])
const lastNormalUrlNode = nodes.findLast((node) => node.type === 'url')
const lastNormalUrl =
typeof lastNormalUrlNode?.data === 'string' ? lastNormalUrlNode.data : undefined
return (
<div className={cn('text-wrap break-words whitespace-pre-wrap', className)}>
{nodes.map((node, index) => {
if (node.type === 'text') {
return node.data
}
if (node.type === 'image' || node.type === 'images') {
const start = imageIndex
const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1)
imageIndex = end
return (
<ImageGallery className="mt-2" key={index} images={allImages} start={start} end={end} />
)
}
if (node.type === 'media') {
return <MediaPlayer className="mt-2" key={index} src={node.data} />
}
if (node.type === 'url') {
return <EmbeddedNormalUrl url={node.data} key={index} />
}
if (node.type === 'invoice') {
return <EmbeddedLNInvoice invoice={node.data} key={index} className="mt-2" />
}
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl url={node.data} key={index} />
const imageInfos = event ? getImageInfosFromEvent(event) : []
const allImages = nodes
.map((node) => {
if (node.type === 'image') {
const imageInfo = imageInfos.find((image) => image.url === node.data)
if (imageInfo) {
return imageInfo
}
const tag = mediaUpload.getImetaTagByUrl(node.data)
return tag
? getImageInfoFromImetaTag(tag, event?.pubkey)
: { url: node.data, pubkey: event?.pubkey }
}
if (node.type === 'event') {
const id = node.data.split(':')[1]
return <EmbeddedNote key={index} noteId={id} className="mt-2" />
}
if (node.type === 'mention') {
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
}
if (node.type === 'hashtag') {
return <EmbeddedHashtag hashtag={node.data} key={index} />
}
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 emoji={emoji} key={index} className="size-4" />
}
if (node.type === 'youtube') {
return <YoutubeEmbeddedPlayer key={index} url={node.data} className="mt-2" />
if (node.type === 'images') {
const urls = Array.isArray(node.data) ? node.data : [node.data]
return urls.map((url) => {
const imageInfo = imageInfos.find((image) => image.url === url)
return imageInfo ?? { url, pubkey: event?.pubkey }
})
}
return null
})}
{lastNormalUrl && <WebPreview className="mt-2" url={lastNormalUrl} />}
</div>
)
})
})
.filter(Boolean)
.flat() as TImageInfo[]
let imageIndex = 0
const emojiInfos = getEmojiInfosFromEmojiTags(event?.tags)
const lastNormalUrlNode = nodes.findLast((node) => node.type === 'url')
const lastNormalUrl =
typeof lastNormalUrlNode?.data === 'string' ? lastNormalUrlNode.data : undefined
return (
<div className={cn('text-wrap break-words whitespace-pre-wrap', className)}>
{nodes.map((node, index) => {
if (node.type === 'text') {
return node.data
}
if (node.type === 'image' || node.type === 'images') {
const start = imageIndex
const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1)
imageIndex = end
return (
<ImageGallery
className="mt-2"
key={index}
images={allImages}
start={start}
end={end}
/>
)
}
if (node.type === 'media') {
return <MediaPlayer className="mt-2" key={index} src={node.data} />
}
if (node.type === 'url') {
return <EmbeddedNormalUrl url={node.data} key={index} />
}
if (node.type === 'invoice') {
return <EmbeddedLNInvoice invoice={node.data} key={index} className="mt-2" />
}
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl url={node.data} key={index} />
}
if (node.type === 'event') {
const id = node.data.split(':')[1]
return <EmbeddedNote key={index} noteId={id} className="mt-2" />
}
if (node.type === 'mention') {
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
}
if (node.type === 'hashtag') {
return <EmbeddedHashtag hashtag={node.data} key={index} />
}
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 emoji={emoji} key={index} />
}
if (node.type === 'youtube') {
return <YoutubeEmbeddedPlayer key={index} url={node.data} className="mt-2" />
}
return null
})}
{lastNormalUrl && <WebPreview className="mt-2" url={lastNormalUrl} />}
</div>
)
}
)
Content.displayName = 'Content'
export default Content

19
src/components/Emoji/index.tsx

@ -5,30 +5,35 @@ import { HTMLAttributes, useState } from 'react' @@ -5,30 +5,35 @@ import { HTMLAttributes, useState } from 'react'
export default function Emoji({
emoji,
className = ''
}: HTMLAttributes<HTMLDivElement> & {
className?: string
classNames
}: Omit<HTMLAttributes<HTMLDivElement>, 'className'> & {
emoji: TEmoji | string
classNames?: {
text?: string
img?: string
}
}) {
const [hasError, setHasError] = useState(false)
if (typeof emoji === 'string') {
return emoji === '+' ? (
<Heart className={cn('size-4 text-red-400 fill-red-400', className)} />
<Heart className={cn('size-4 text-red-400 fill-red-400', classNames?.img)} />
) : (
<span className={cn('whitespace-nowrap', className)}>{emoji}</span>
<span className={cn('whitespace-nowrap', classNames?.text)}>{emoji}</span>
)
}
if (hasError) {
return <span className={cn('whitespace-nowrap', className)}>{`:${emoji.shortcode}:`}</span>
return (
<span className={cn('whitespace-nowrap', classNames?.text)}>{`:${emoji.shortcode}:`}</span>
)
}
return (
<img
src={emoji.url}
alt={emoji.shortcode}
className={cn('inline-block size-4', className)}
className={cn('inline-block size-4', classNames?.img)}
onLoad={() => {
setHasError(false)
}}

5
src/components/NoteInteractions/Tabs.tsx

@ -2,9 +2,12 @@ import { cn } from '@/lib/utils' @@ -2,9 +2,12 @@ import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import { useRef, useEffect, useState } from 'react'
export type TTabValue = 'replies' | 'quotes'
export type TTabValue = 'replies' | 'quotes' | 'reactions' | 'reposts' | 'zaps'
const TABS = [
{ value: 'replies', label: 'Replies' },
{ value: 'zaps', label: 'Zaps' },
{ value: 'reposts', label: 'Reposts' },
{ value: 'reactions', label: 'Reactions' },
{ value: 'quotes', label: 'Quotes' }
] as { value: TTabValue; label: string }[]

42
src/components/NoteInteractions/index.tsx

@ -1,9 +1,13 @@ @@ -1,9 +1,13 @@
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Event } from 'nostr-tools'
import { useState } from 'react'
import HideUntrustedContentButton from '../HideUntrustedContentButton'
import QuoteList from '../QuoteList'
import ReactionList from '../ReactionList'
import ReplyNoteList from '../ReplyNoteList'
import RepostList from '../RepostList'
import ZapList from '../ZapList'
import { Tabs, TTabValue } from './Tabs'
export default function NoteInteractions({
@ -14,19 +18,41 @@ export default function NoteInteractions({ @@ -14,19 +18,41 @@ export default function NoteInteractions({
event: Event
}) {
const [type, setType] = useState<TTabValue>('replies')
let list
switch (type) {
case 'replies':
list = <ReplyNoteList index={pageIndex} event={event} />
break
case 'quotes':
list = <QuoteList event={event} />
break
case 'reactions':
list = <ReactionList event={event} />
break
case 'reposts':
list = <RepostList event={event} />
break
case 'zaps':
list = <ZapList event={event} />
break
default:
break
}
return (
<>
<div className="flex items-center justify-between pr-1">
<Tabs selectedTab={type} onTabChange={setType} />
<HideUntrustedContentButton type="interactions" />
<div className="flex items-center justify-between">
<ScrollArea className="flex-1 w-0">
<Tabs selectedTab={type} onTabChange={setType} />
<ScrollBar orientation="horizontal" className="opacity-0" />
</ScrollArea>
<Separator orientation="vertical" className="h-6" />
<div className="size-10 flex items-center justify-center">
<HideUntrustedContentButton type="interactions" />
</div>
</div>
<Separator />
{type === 'replies' ? (
<ReplyNoteList index={pageIndex} event={event} />
) : (
<QuoteList event={event} />
)}
{list}
</>
)
}

4
src/components/NoteStats/TopZaps.tsx

@ -22,14 +22,14 @@ export default function TopZaps({ event }: { event: Event }) { @@ -22,14 +22,14 @@ export default function TopZaps({ event }: { event: Event }) {
{topZaps.map((zap, index) => (
<div
key={zap.pr}
className="flex gap-1 py-1 pl-1 pr-2 text-sm rounded-full bg-muted/80 items-center text-yellow-400 border border-yellow-400 hover:bg-yellow-400/20 cursor-pointer"
className="flex gap-1 py-1 pl-1 pr-2 text-sm max-w-72 rounded-full bg-muted/80 items-center text-yellow-400 border border-yellow-400 hover:bg-yellow-400/20 cursor-pointer"
onClick={(e) => {
e.stopPropagation()
setZapIndex(index)
}}
>
<SimpleUserAvatar userId={zap.pubkey} size="xSmall" />
<Zap className="size-3 fill-yellow-400" />
<Zap className="size-3 fill-yellow-400 shrink-0" />
<div className="font-semibold">{formatAmount(zap.amount)}</div>
<div className="truncate">{zap.comment}</div>
<div onClick={(e) => e.stopPropagation()}>

7
src/components/QuoteList/index.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service'
@ -38,7 +39,9 @@ export default function QuoteList({ event, className }: { event: Event; classNam @@ -38,7 +39,9 @@ export default function QuoteList({ event, className }: { event: Event; classNam
{
urls: relayUrls,
filter: {
'#q': [event.id],
'#q': [
isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) : event.id
],
kinds: [
kinds.ShortTextNote,
kinds.Highlights,
@ -130,7 +133,7 @@ export default function QuoteList({ event, className }: { event: Event; classNam @@ -130,7 +133,7 @@ export default function QuoteList({ event, className }: { event: Event; classNam
return (
<div className={className}>
<div className="min-h-screen">
<div className="min-h-[80vh]">
<div>
{events.slice(0, showCount).map((event) => {
if (hideUntrustedInteractions && !isUserTrusted(event.pubkey)) {

89
src/components/ReactionList/index.tsx

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
import { useSecondaryPage } from '@/PageManager'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { toProfile } from '@/lib/link'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import { Event } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Emoji from '../Emoji'
import { FormattedTimestamp } from '../FormattedTimestamp'
import Nip05 from '../Nip05'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
const SHOW_COUNT = 20
export default function ReactionList({ event }: { event: Event }) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { isSmallScreen } = useScreenSize()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const noteStats = useNoteStatsById(event.id)
const filteredLikes = useMemo(() => {
return (noteStats?.likes ?? [])
.filter((like) => !hideUntrustedInteractions || isUserTrusted(like.pubkey))
.sort((a, b) => b.created_at - a.created_at)
}, [noteStats, event.id, hideUntrustedInteractions, isUserTrusted])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const bottomRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (!bottomRef.current || filteredLikes.length <= showCount) return
const obs = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setShowCount((c) => c + SHOW_COUNT)
},
{ rootMargin: '10px', threshold: 0.1 }
)
obs.observe(bottomRef.current)
return () => obs.disconnect()
}, [filteredLikes.length, showCount])
return (
<div className="min-h-[80vh]">
{filteredLikes.slice(0, showCount).map((like) => (
<div
key={like.id}
className="px-4 py-3 border-b transition-colors clickable flex items-center gap-3"
onClick={() => push(toProfile(like.pubkey))}
>
<div className="w-6 flex flex-col items-center">
<Emoji
emoji={like.emoji}
classNames={{
text: 'text-xl',
img: 'size-5'
}}
/>
</div>
<UserAvatar userId={like.pubkey} size="medium" className="shrink-0" />
<div className="flex-1 w-0">
<Username
userId={like.pubkey}
className="text-sm font-semibold text-muted-foreground hover:text-foreground max-w-fit truncate"
skeletonClassName="h-3"
/>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Nip05 pubkey={like.pubkey} append="·" />
<FormattedTimestamp
timestamp={like.created_at}
className="shrink-0"
short={isSmallScreen}
/>
</div>
</div>
</div>
))}
<div ref={bottomRef} />
<div className="text-sm mt-2 text-center text-muted-foreground">
{filteredLikes.length > 0 ? t('No more reactions') : t('No reactions yet')}
</div>
</div>
)
}

2
src/components/ReplyNoteList/index.tsx

@ -274,7 +274,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event: @@ -274,7 +274,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
}, [])
return (
<div className="min-h-screen">
<div className="min-h-[80vh]">
{loading && (replies.length === 0 ? <ReplyNoteSkeleton /> : <LoadingBar />)}
{!loading && until && (
<div

81
src/components/RepostList/index.tsx

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
import { useSecondaryPage } from '@/PageManager'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { toProfile } from '@/lib/link'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import { Repeat } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FormattedTimestamp } from '../FormattedTimestamp'
import Nip05 from '../Nip05'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
const SHOW_COUNT = 20
export default function RepostList({ event }: { event: Event }) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { isSmallScreen } = useScreenSize()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const noteStats = useNoteStatsById(event.id)
const filteredReposts = useMemo(() => {
return (noteStats?.reposts ?? [])
.filter((repost) => !hideUntrustedInteractions || isUserTrusted(repost.pubkey))
.sort((a, b) => b.created_at - a.created_at)
}, [noteStats, event.id, hideUntrustedInteractions, isUserTrusted])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const bottomRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (!bottomRef.current || filteredReposts.length <= showCount) return
const obs = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setShowCount((c) => c + SHOW_COUNT)
},
{ rootMargin: '10px', threshold: 0.1 }
)
obs.observe(bottomRef.current)
return () => obs.disconnect()
}, [filteredReposts.length, showCount])
return (
<div className="min-h-[80vh]">
{filteredReposts.slice(0, showCount).map((repost) => (
<div
key={repost.id}
className="px-4 py-3 border-b transition-colors clickable flex items-center gap-3"
onClick={() => push(toProfile(repost.pubkey))}
>
<Repeat className="text-green-400 size-5" />
<UserAvatar userId={repost.pubkey} size="medium" className="shrink-0" />
<div className="flex-1 w-0">
<Username
userId={repost.pubkey}
className="text-sm font-semibold text-muted-foreground hover:text-foreground max-w-fit truncate"
skeletonClassName="h-3"
/>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Nip05 pubkey={repost.pubkey} append="·" />
<FormattedTimestamp
timestamp={repost.created_at}
className="shrink-0"
short={isSmallScreen}
/>
</div>
</div>
</div>
))}
<div ref={bottomRef} />
<div className="text-sm mt-2 text-center text-muted-foreground">
{filteredReposts.length > 0 ? t('No more reposts') : t('No reposts yet')}
</div>
</div>
)
}

84
src/components/ZapList/index.tsx

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
import { useSecondaryPage } from '@/PageManager'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { formatAmount } from '@/lib/lightning'
import { toProfile } from '@/lib/link'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Zap } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Content from '../Content'
import { FormattedTimestamp } from '../FormattedTimestamp'
import Nip05 from '../Nip05'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
const SHOW_COUNT = 20
export default function ZapList({ event }: { event: Event }) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { isSmallScreen } = useScreenSize()
const noteStats = useNoteStatsById(event.id)
const filteredZaps = useMemo(() => {
return (noteStats?.zaps ?? []).sort((a, b) => b.amount - a.amount)
}, [noteStats, event.id])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const bottomRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (!bottomRef.current || filteredZaps.length <= showCount) return
const obs = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setShowCount((c) => c + SHOW_COUNT)
},
{ rootMargin: '10px', threshold: 0.1 }
)
obs.observe(bottomRef.current)
return () => obs.disconnect()
}, [filteredZaps.length, showCount])
return (
<div className="min-h-[80vh]">
{filteredZaps.slice(0, showCount).map((zap) => (
<div
key={zap.pr}
className="px-4 py-3 border-b transition-colors clickable flex gap-2"
onClick={() => push(toProfile(zap.pubkey))}
>
<div className="w-8 flex flex-col items-center mt-0.5">
<Zap className="text-yellow-400 size-5" />
<div className="text-sm font-semibold text-yellow-400">{formatAmount(zap.amount)}</div>
</div>
<div className="flex space-x-2 items-start">
<UserAvatar userId={zap.pubkey} size="medium" className="shrink-0 mt-0.5" />
<div className="flex-1">
<Username
userId={zap.pubkey}
className="text-sm font-semibold text-muted-foreground hover:text-foreground max-w-fit truncate"
skeletonClassName="h-3"
/>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Nip05 pubkey={zap.pubkey} append="·" />
<FormattedTimestamp
timestamp={zap.created_at}
className="shrink-0"
short={isSmallScreen}
/>
</div>
<Content className="mt-2" content={zap.comment} />
</div>
</div>
</div>
))}
<div ref={bottomRef} />
<div className="text-sm mt-2 text-center text-muted-foreground">
{filteredZaps.length > 0 ? t('No more zaps') : t('No zaps yet')}
</div>
</div>
)
}

9
src/i18n/locales/ar.ts

@ -326,6 +326,13 @@ export default { @@ -326,6 +326,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'تم البث بنجاح إلى المرحل: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'فشل البث إلى المرحل: {{url}}. خطأ: {{error}}',
'Write relays': 'مرحلات الكتابة'
'Write relays': 'مرحلات الكتابة',
'No more reactions': 'لا توجد تفاعلات إضافية',
'No reactions yet': 'لا توجد تفاعلات بعد',
'No more zaps': 'لا توجد مزيد من الزابس',
'No zaps yet': 'لا توجد زابس بعد',
'No more reposts': 'لا توجد مزيد من إعادة النشر',
'No reposts yet': 'لا توجد إعادة نشر بعد',
Reposts: 'إعادة النشر'
}
}

9
src/i18n/locales/de.ts

@ -333,6 +333,13 @@ export default { @@ -333,6 +333,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'Erfolgreich an Relay gesendet: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'Fehler beim Senden an Relay: {{url}}. Fehler: {{error}}',
'Write relays': 'Schreib-Relays'
'Write relays': 'Schreib-Relays',
'No more reactions': 'Keine weiteren Reaktionen',
'No reactions yet': 'Noch keine Reaktionen',
'No more zaps': 'Keine weiteren Zaps',
'No zaps yet': 'Noch keine Zaps',
'No more reposts': 'Keine weiteren Reposts',
'No reposts yet': 'Noch keine Reposts',
Reposts: 'Reposts'
}
}

9
src/i18n/locales/en.ts

@ -327,6 +327,13 @@ export default { @@ -327,6 +327,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'Successfully broadcasted to relay: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'Failed to broadcast to relay: {{url}}. Error: {{error}}',
'Write relays': 'Write relays'
'Write relays': 'Write relays',
'No more reactions': 'No more reactions',
'No reactions yet': 'No reactions yet',
'No more zaps': 'No more zaps',
'No zaps yet': 'No zaps yet',
'No more reposts': 'No more reposts',
'No reposts yet': 'No reposts yet',
Reposts: 'Reposts'
}
}

9
src/i18n/locales/es.ts

@ -332,6 +332,13 @@ export default { @@ -332,6 +332,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'Transmitido exitosamente al relé: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'Error al transmitir al relé: {{url}}. Error: {{error}}',
'Write relays': 'Relés de escritura'
'Write relays': 'Relés de escritura',
'No more reactions': 'No hay más reacciones',
'No reactions yet': 'Sin reacciones aún',
'No more zaps': 'No hay más zaps',
'No zaps yet': 'Sin zaps aún',
'No more reposts': 'No hay más reposts',
'No reposts yet': 'Sin reposts aún',
Reposts: 'Reposts'
}
}

9
src/i18n/locales/fa.ts

@ -327,6 +327,13 @@ export default { @@ -327,6 +327,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'با موفقیت به رله پخش شد: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'پخش به رله ناموفق بود: {{url}}. خطا: {{error}}',
'Write relays': 'رلههای نوشتن'
'Write relays': 'رلههای نوشتن',
'No more reactions': 'هیچ واکنشی بیشتر وجود ندارد',
'No reactions yet': 'هنوز هیچ واکنشی وجود ندارد',
'No more zaps': 'هیچ زپی بیشتر وجود ندارد',
'No zaps yet': 'هنوز هیچ زپی وجود ندارد',
'No more reposts': 'هیچ بازنشر بیشتری وجود ندارد',
'No reposts yet': 'هنوز هیچ بازنشر وجود ندارد',
Reposts: 'بازنشرها'
}
}

9
src/i18n/locales/fr.ts

@ -332,6 +332,13 @@ export default { @@ -332,6 +332,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'Diffusion réussie vers le relais : {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'Échec de la diffusion vers le relais : {{url}}. Erreur : {{error}}',
'Write relays': 'Relais d’écriture'
'Write relays': 'Relais d’écriture',
'No more reactions': 'Plus de réactions',
'No reactions yet': 'Pas encore de réactions',
'No more zaps': 'Plus de zaps',
'No zaps yet': 'Pas encore de zaps',
'No more reposts': 'Plus de reposts',
'No reposts yet': 'Pas encore de reposts',
Reposts: 'Reposts'
}
}

9
src/i18n/locales/it.ts

@ -331,6 +331,13 @@ export default { @@ -331,6 +331,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'Trasmesso con successo al relay: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'Errore nella trasmissione al relay: {{url}}. Errore: {{error}}',
'Write relays': 'Relay di scrittura'
'Write relays': 'Relay di scrittura',
'No more reactions': 'Non ci sono più reazioni',
'No reactions yet': 'Ancora nessuna reazione',
'No more zaps': 'Non ci sono più zaps',
'No zaps yet': 'Ancora nessuno zap',
'No more reposts': 'Non ci sono più repost',
'No reposts yet': 'Ancora nessun repost',
Reposts: 'Repost'
}
}

9
src/i18n/locales/ja.ts

@ -329,6 +329,13 @@ export default { @@ -329,6 +329,13 @@ export default {
'リレイへのブロードキャストが成功しました:{{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'リレイへのブロードキャストが失敗しました:{{url}}。エラー:{{error}}',
'Write relays': '書きリレイ'
'Write relays': '書きリレイ',
'No more reactions': 'これ以上の反応はありません',
'No reactions yet': 'まだ反応はありません',
'No more zaps': 'これ以上のZapはありません',
'No zaps yet': 'まだZapはありません',
'No more reposts': 'これ以上のリポストはありません',
'No reposts yet': 'まだリポストはありません',
Reposts: 'リポスト'
}
}

9
src/i18n/locales/ko.ts

@ -328,6 +328,13 @@ export default { @@ -328,6 +328,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': '릴레이로 브로드캐스트에 성공했습니다: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'릴레이로 브로드캐스트에 실패했습니다: {{url}}. 오류: {{error}}',
'Write relays': '쓰기 릴레이'
'Write relays': '쓰기 릴레이',
'No more reactions': '더 이상 반응이 없습니다',
'No reactions yet': '아직 반응이 없습니다',
'No more zaps': '더 이상 즙이 없습니다',
'No zaps yet': '아직 즙이 없습니다',
'No more reposts': '더 이상 리포스트가 없습니다',
'No reposts yet': '아직 리포스트가 없습니다',
Reposts: '리포스트'
}
}

9
src/i18n/locales/pl.ts

@ -331,6 +331,13 @@ export default { @@ -331,6 +331,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'Pomyślnie transmitowano do przekaźnika: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'Nie udało się transmitować do przekaźnika: {{url}}. Błąd: {{error}}',
'Write relays': 'Przekaźniki zapisu'
'Write relays': 'Przekaźniki zapisu',
'No more reactions': 'Brak kolejnych reakcji',
'No reactions yet': 'Brak reakcji',
'No more zaps': 'Brak kolejnych zapów',
'No zaps yet': 'Brak zapów',
'No more reposts': 'Brak kolejnych repostów',
'No reposts yet': 'Brak repostów',
Reposts: 'Reposty'
}
}

9
src/i18n/locales/pt-BR.ts

@ -330,6 +330,13 @@ export default { @@ -330,6 +330,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'Transmitido com sucesso para o relay: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'Falha ao transmitir para o relay: {{url}}. Erro: {{error}}',
'Write relays': 'Relés de escrita'
'Write relays': 'Relés de escrita',
'No more reactions': 'Sem mais reações',
'No reactions yet': 'Ainda sem reações',
'No more zaps': 'Sem mais zaps',
'No zaps yet': 'Ainda sem zaps',
'No more reposts': 'Sem mais reposts',
'No reposts yet': 'Ainda sem reposts',
Reposts: 'Reposts'
}
}

9
src/i18n/locales/pt-PT.ts

@ -331,6 +331,13 @@ export default { @@ -331,6 +331,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'Transmitido com sucesso para o relay: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'Falha ao transmitir para o relay: {{url}}. Erro: {{error}}',
'Write relays': 'Relés de escrita'
'Write relays': 'Relés de escrita',
'No more reactions': 'Sem mais reações',
'No reactions yet': 'Ainda sem reações',
'No more zaps': 'Sem mais zaps',
'No zaps yet': 'Ainda sem zaps',
'No more reposts': 'Sem mais reposts',
'No reposts yet': 'Ainda sem reposts',
Reposts: 'Reposts'
}
}

9
src/i18n/locales/ru.ts

@ -331,6 +331,13 @@ export default { @@ -331,6 +331,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'Успешно транслировано в релей: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'Ошибка трансляции в релей: {{url}}. Ошибка: {{error}}',
'Write relays': 'Ретрансляторы для записи'
'Write relays': 'Ретрансляторы для записи',
'No more reactions': 'Больше нет реакций',
'No reactions yet': 'Пока нет реакций',
'No more zaps': 'Больше нет запов',
'No zaps yet': 'Пока нет запов',
'No more reposts': 'Больше нет репостов',
'No reposts yet': 'Пока нет репостов',
Reposts: 'Репосты'
}
}

9
src/i18n/locales/th.ts

@ -325,6 +325,13 @@ export default { @@ -325,6 +325,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': 'สงสญญาณไปยงรเลยสำเรจแลว: {{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'การสงสญญาณไปยงรเลยมเหลว: {{url}} ขอผดพลาด: {{error}}',
'Write relays': 'รเลยการเขยน'
'Write relays': 'รเลยการเขยน',
'No more reactions': 'ไมปฏยาเพมเตม',
'No reactions yet': 'ยงไมปฏยา',
'No more zaps': 'ไมซาตสเพมเตม',
'No zaps yet': 'ยงไมซาตส',
'No more reposts': 'ไมการรโพสตเพมเตม',
'No reposts yet': 'ยงไมการรโพสต',
Reposts: 'การรโพสต'
}
}

9
src/i18n/locales/zh.ts

@ -324,6 +324,13 @@ export default { @@ -324,6 +324,13 @@ export default {
'Successfully broadcasted to relay: {{url}}': '成功广播到服务器:{{url}}',
'Failed to broadcast to relay: {{url}}. Error: {{error}}':
'广播到服务器失败:{{url}}。错误:{{error}}',
'Write relays': '写服务器'
'Write relays': '写服务器',
'No more reactions': '没有更多互动了',
'No reactions yet': '暂无互动',
'No more zaps': '没有更多打闪了',
'No zaps yet': '暂无打闪',
'No more reposts': '没有更多转发了',
'No reposts yet': '暂无转发',
Reposts: '转发'
}
}

78
src/lib/draft-event.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { ApplicationDataKey, ExtendedKind, POLL_TYPE } from '@/constants'
import { ApplicationDataKey, EMBEDDED_EVENT_REGEX, ExtendedKind, POLL_TYPE } from '@/constants'
import client from '@/services/client.service'
import mediaUpload from '@/services/media-upload.service'
import {
@ -12,6 +12,7 @@ import { @@ -12,6 +12,7 @@ import {
import dayjs from 'dayjs'
import { Event, kinds, nip19 } from 'nostr-tools'
import {
getReplaceableCoordinate,
getReplaceableCoordinateFromEvent,
getRootETag,
isProtectedEvent,
@ -54,6 +55,10 @@ export function createRepostDraftEvent(event: Event): TDraftEvent { @@ -54,6 +55,10 @@ export function createRepostDraftEvent(event: Event): TDraftEvent {
const isProtected = isProtectedEvent(event)
const tags = [buildETag(event.id, event.pubkey), buildPTag(event.pubkey)]
if (isReplaceableEvent(event.kind)) {
tags.push(buildATag(event))
}
return {
kind: kinds.Repost,
content: isProtected ? '' : JSON.stringify(event),
@ -73,10 +78,8 @@ export async function createShortTextNoteDraftEvent( @@ -73,10 +78,8 @@ export async function createShortTextNoteDraftEvent(
isNsfw?: boolean
} = {}
): Promise<TDraftEvent> {
const { quoteEventIds, rootETag, parentETag } = await extractRelatedEventIds(
content,
options.parentEvent
)
const { quoteEventHexIds, quoteReplaceableCoordinates, rootETag, parentETag } =
await extractRelatedEventIds(content, options.parentEvent)
const hashtags = extractHashtags(content)
const tags = hashtags.map((hashtag) => buildTTag(hashtag))
@ -88,7 +91,8 @@ export async function createShortTextNoteDraftEvent( @@ -88,7 +91,8 @@ export async function createShortTextNoteDraftEvent(
}
// q tags
tags.push(...quoteEventIds.map((eventId) => buildQTag(eventId)))
tags.push(...quoteEventHexIds.map((eventId) => buildQTag(eventId)))
tags.push(...quoteReplaceableCoordinates.map((coordinate) => buildReplaceableQTag(coordinate)))
// e tags
if (rootETag.length) {
@ -153,7 +157,7 @@ export async function createPictureNoteDraftEvent( @@ -153,7 +157,7 @@ export async function createPictureNoteDraftEvent(
protectedEvent?: boolean
} = {}
): Promise<TDraftEvent> {
const { quoteEventIds } = await extractRelatedEventIds(content)
const { quoteEventHexIds, quoteReplaceableCoordinates } = await extractRelatedEventIds(content)
const hashtags = extractHashtags(content)
if (!pictureInfos.length) {
throw new Error('No images found in content')
@ -162,7 +166,8 @@ export async function createPictureNoteDraftEvent( @@ -162,7 +166,8 @@ export async function createPictureNoteDraftEvent(
const tags = pictureInfos
.map((info) => buildImetaTag(info.tags))
.concat(hashtags.map((hashtag) => buildTTag(hashtag)))
.concat(quoteEventIds.map((eventId) => buildQTag(eventId)))
.concat(quoteEventHexIds.map((eventId) => buildQTag(eventId)))
.concat(quoteReplaceableCoordinates.map((coordinate) => buildReplaceableQTag(coordinate)))
.concat(mentions.map((pubkey) => buildPTag(pubkey)))
if (options.addClientTag) {
@ -192,13 +197,21 @@ export async function createCommentDraftEvent( @@ -192,13 +197,21 @@ export async function createCommentDraftEvent(
isNsfw?: boolean
} = {}
): Promise<TDraftEvent> {
const { quoteEventIds, rootEventId, rootCoordinateTag, rootKind, rootPubkey, rootUrl } =
await extractCommentMentions(content, parentEvent)
const {
quoteEventHexIds,
quoteReplaceableCoordinates,
rootEventId,
rootCoordinateTag,
rootKind,
rootPubkey,
rootUrl
} = await extractCommentMentions(content, parentEvent)
const hashtags = extractHashtags(content)
const tags = hashtags
.map((hashtag) => buildTTag(hashtag))
.concat(quoteEventIds.map((eventId) => buildQTag(eventId)))
.concat(quoteEventHexIds.map((eventId) => buildQTag(eventId)))
.concat(quoteReplaceableCoordinates.map((coordinate) => buildReplaceableQTag(coordinate)))
const images = extractImagesFromContent(content)
if (images && images.length) {
@ -357,7 +370,7 @@ export async function createPollDraftEvent( @@ -357,7 +370,7 @@ export async function createPollDraftEvent(
isNsfw?: boolean
} = {}
): Promise<TDraftEvent> {
const { quoteEventIds } = await extractRelatedEventIds(question)
const { quoteEventHexIds, quoteReplaceableCoordinates } = await extractRelatedEventIds(question)
const hashtags = extractHashtags(question)
const tags = hashtags.map((hashtag) => buildTTag(hashtag))
@ -369,7 +382,8 @@ export async function createPollDraftEvent( @@ -369,7 +382,8 @@ export async function createPollDraftEvent(
}
// q tags
tags.push(...quoteEventIds.map((eventId) => buildQTag(eventId)))
tags.push(...quoteEventHexIds.map((eventId) => buildQTag(eventId)))
tags.push(...quoteReplaceableCoordinates.map((coordinate) => buildReplaceableQTag(coordinate)))
// p tags
tags.push(...mentions.map((pubkey) => buildPTag(pubkey)))
@ -441,10 +455,11 @@ function generateImetaTags(imageUrls: string[]) { @@ -441,10 +455,11 @@ function generateImetaTags(imageUrls: string[]) {
}
async function extractRelatedEventIds(content: string, parentEvent?: Event) {
const quoteEventIds: string[] = []
const quoteEventHexIds: string[] = []
const quoteReplaceableCoordinates: string[] = []
let rootETag: string[] = []
let parentETag: string[] = []
const matches = content.match(/nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g)
const matches = content.match(EMBEDDED_EVENT_REGEX)
const addToSet = (arr: string[], item: string) => {
if (!arr.includes(item)) arr.push(item)
@ -455,9 +470,14 @@ async function extractRelatedEventIds(content: string, parentEvent?: Event) { @@ -455,9 +470,14 @@ async function extractRelatedEventIds(content: string, parentEvent?: Event) {
const id = m.split(':')[1]
const { type, data } = nip19.decode(id)
if (type === 'nevent') {
addToSet(quoteEventIds, data.id)
addToSet(quoteEventHexIds, data.id)
} else if (type === 'note') {
addToSet(quoteEventIds, data)
addToSet(quoteEventHexIds, data)
} else if (type === 'naddr') {
addToSet(
quoteReplaceableCoordinates,
getReplaceableCoordinate(data.kind, data.pubkey, data.identifier)
)
}
} catch (e) {
console.error(e)
@ -486,14 +506,16 @@ async function extractRelatedEventIds(content: string, parentEvent?: Event) { @@ -486,14 +506,16 @@ async function extractRelatedEventIds(content: string, parentEvent?: Event) {
}
return {
quoteEventIds,
quoteEventHexIds,
quoteReplaceableCoordinates,
rootETag,
parentETag
}
}
async function extractCommentMentions(content: string, parentEvent: Event) {
const quoteEventIds: string[] = []
const quoteEventHexIds: string[] = []
const quoteReplaceableCoordinates: string[] = []
const isComment = [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT].includes(parentEvent.kind)
const rootCoordinateTag = isComment
? parentEvent.tags.find(tagNameEquals('A'))
@ -509,15 +531,20 @@ async function extractCommentMentions(content: string, parentEvent: Event) { @@ -509,15 +531,20 @@ async function extractCommentMentions(content: string, parentEvent: Event) {
if (!arr.includes(item)) arr.push(item)
}
const matches = content.match(/nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g)
const matches = content.match(EMBEDDED_EVENT_REGEX)
for (const m of matches || []) {
try {
const id = m.split(':')[1]
const { type, data } = nip19.decode(id)
if (type === 'nevent') {
addToSet(quoteEventIds, data.id)
addToSet(quoteEventHexIds, data.id)
} else if (type === 'note') {
addToSet(quoteEventIds, data)
addToSet(quoteEventHexIds, data)
} else if (type === 'naddr') {
addToSet(
quoteReplaceableCoordinates,
getReplaceableCoordinate(data.kind, data.pubkey, data.identifier)
)
}
} catch (e) {
console.error(e)
@ -525,7 +552,8 @@ async function extractCommentMentions(content: string, parentEvent: Event) { @@ -525,7 +552,8 @@ async function extractCommentMentions(content: string, parentEvent: Event) {
}
return {
quoteEventIds,
quoteEventHexIds,
quoteReplaceableCoordinates,
rootEventId,
rootCoordinateTag,
rootKind,
@ -601,6 +629,10 @@ function buildQTag(eventHexId: string) { @@ -601,6 +629,10 @@ function buildQTag(eventHexId: string) {
return trimTagEnd(['q', eventHexId, client.getEventHint(eventHexId)]) // TODO: pubkey
}
function buildReplaceableQTag(coordinate: string) {
return trimTagEnd(['q', coordinate])
}
function buildRTag(url: string, scope: TMailboxRelayScope) {
return scope === 'both' ? ['r', url, scope] : ['r', url]
}

67
src/services/note-stats.service.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { BIG_RELAY_URLS } from '@/constants'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag'
import client from '@/services/client.service'
@ -10,8 +11,9 @@ export type TNoteStats = { @@ -10,8 +11,9 @@ export type TNoteStats = {
likeIdSet: Set<string>
likes: { id: string; pubkey: string; created_at: number; emoji: TEmoji | string }[]
repostPubkeySet: Set<string>
reposts: { id: string; pubkey: string; created_at: number }[]
zapPrSet: Set<string>
zaps: { pr: string; pubkey: string; amount: number; comment?: string }[]
zaps: { pr: string; pubkey: string; amount: number; created_at: number; comment?: string }[]
updatedAt?: number
}
@ -37,6 +39,11 @@ class NoteStatsService { @@ -37,6 +39,11 @@ class NoteStatsService {
client.fetchRelayList(event.pubkey),
client.fetchProfile(event.pubkey)
])
const replaceableCoordinate = isReplaceableEvent(event.kind)
? getReplaceableCoordinateFromEvent(event)
: undefined
const filters: Filter[] = [
{
'#e': [event.id],
@ -50,12 +57,35 @@ class NoteStatsService { @@ -50,12 +57,35 @@ class NoteStatsService {
}
]
if (replaceableCoordinate) {
filters.push(
{
'#a': [replaceableCoordinate],
kinds: [kinds.Reaction],
limit: 500
},
{
'#a': [replaceableCoordinate],
kinds: [kinds.Repost],
limit: 100
}
)
}
if (authorProfile?.lightningAddress) {
filters.push({
'#e': [event.id],
kinds: [kinds.Zap],
limit: 500
})
if (replaceableCoordinate) {
filters.push({
'#a': [replaceableCoordinate],
kinds: [kinds.Zap],
limit: 500
})
}
}
if (pubkey) {
@ -65,12 +95,28 @@ class NoteStatsService { @@ -65,12 +95,28 @@ class NoteStatsService {
kinds: [kinds.Reaction, kinds.Repost]
})
if (replaceableCoordinate) {
filters.push({
'#a': [replaceableCoordinate],
authors: [pubkey],
kinds: [kinds.Reaction, kinds.Repost]
})
}
if (authorProfile?.lightningAddress) {
filters.push({
'#e': [event.id],
'#P': [pubkey],
kinds: [kinds.Zap]
})
if (replaceableCoordinate) {
filters.push({
'#a': [replaceableCoordinate],
'#P': [pubkey],
kinds: [kinds.Zap]
})
}
}
}
@ -123,6 +169,7 @@ class NoteStatsService { @@ -123,6 +169,7 @@ class NoteStatsService {
pr: string,
amount: number,
comment?: string,
created_at: number = dayjs().unix(),
notify: boolean = true
) {
const old = this.noteStatsMap.get(eventId) || {}
@ -131,7 +178,7 @@ class NoteStatsService { @@ -131,7 +178,7 @@ class NoteStatsService {
if (zapPrSet.has(pr)) return
zapPrSet.add(pr)
zaps.push({ pr, pubkey, amount, comment })
zaps.push({ pr, pubkey, amount, comment, created_at })
this.noteStatsMap.set(eventId, { ...old, zapPrSet, zaps })
if (notify) {
this.notifyNoteStats(eventId)
@ -194,8 +241,12 @@ class NoteStatsService { @@ -194,8 +241,12 @@ class NoteStatsService {
const old = this.noteStatsMap.get(eventId) || {}
const repostPubkeySet = old.repostPubkeySet || new Set()
const reposts = old.reposts || []
if (repostPubkeySet.has(evt.pubkey)) return
repostPubkeySet.add(evt.pubkey)
this.noteStatsMap.set(eventId, { ...old, repostPubkeySet })
reposts.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
this.noteStatsMap.set(eventId, { ...old, repostPubkeySet, reposts })
return eventId
}
@ -205,7 +256,15 @@ class NoteStatsService { @@ -205,7 +256,15 @@ class NoteStatsService {
const { originalEventId, senderPubkey, invoice, amount, comment } = info
if (!originalEventId || !senderPubkey) return
return this.addZap(senderPubkey, originalEventId, invoice, amount, comment, false)
return this.addZap(
senderPubkey,
originalEventId,
invoice,
amount,
comment,
evt.created_at,
false
)
}
}

Loading…
Cancel
Save