diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx
index 6c5d39f..b9022d0 100644
--- a/src/components/Content/index.tsx
+++ b/src/components/Content/index.tsx
@@ -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 (
-
- {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 (
-
- )
- }
- if (node.type === 'media') {
- return
- }
- if (node.type === 'url') {
- return
- }
- if (node.type === 'invoice') {
- return
- }
- if (node.type === 'websocket-url') {
- return
+ 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
- }
- if (node.type === 'mention') {
- return
- }
- if (node.type === 'hashtag') {
- 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
- }
- if (node.type === 'youtube') {
- return
+ 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 && }
-
- )
-})
+ })
+ .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 (
+
+ {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 (
+
+ )
+ }
+ if (node.type === 'media') {
+ return
+ }
+ if (node.type === 'url') {
+ return
+ }
+ if (node.type === 'invoice') {
+ return
+ }
+ if (node.type === 'websocket-url') {
+ return
+ }
+ if (node.type === 'event') {
+ const id = node.data.split(':')[1]
+ return
+ }
+ if (node.type === 'mention') {
+ return
+ }
+ if (node.type === 'hashtag') {
+ 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
+ }
+ if (node.type === 'youtube') {
+ return
+ }
+ return null
+ })}
+ {lastNormalUrl && }
+
+ )
+ }
+)
Content.displayName = 'Content'
export default Content
diff --git a/src/components/Emoji/index.tsx b/src/components/Emoji/index.tsx
index 66715db..c936168 100644
--- a/src/components/Emoji/index.tsx
+++ b/src/components/Emoji/index.tsx
@@ -5,30 +5,35 @@ import { HTMLAttributes, useState } from 'react'
export default function Emoji({
emoji,
- className = ''
-}: HTMLAttributes & {
- className?: string
+ classNames
+}: Omit, 'className'> & {
emoji: TEmoji | string
+ classNames?: {
+ text?: string
+ img?: string
+ }
}) {
const [hasError, setHasError] = useState(false)
if (typeof emoji === 'string') {
return emoji === '+' ? (
-
+
) : (
- {emoji}
+ {emoji}
)
}
if (hasError) {
- return {`:${emoji.shortcode}:`}
+ return (
+ {`:${emoji.shortcode}:`}
+ )
}
return (
{
setHasError(false)
}}
diff --git a/src/components/NoteInteractions/Tabs.tsx b/src/components/NoteInteractions/Tabs.tsx
index 9b13f73..1c4ba58 100644
--- a/src/components/NoteInteractions/Tabs.tsx
+++ b/src/components/NoteInteractions/Tabs.tsx
@@ -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 }[]
diff --git a/src/components/NoteInteractions/index.tsx b/src/components/NoteInteractions/index.tsx
index fdecccd..3538031 100644
--- a/src/components/NoteInteractions/index.tsx
+++ b/src/components/NoteInteractions/index.tsx
@@ -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({
event: Event
}) {
const [type, setType] = useState('replies')
+ let list
+ switch (type) {
+ case 'replies':
+ list =
+ break
+ case 'quotes':
+ list =
+ break
+ case 'reactions':
+ list =
+ break
+ case 'reposts':
+ list =
+ break
+ case 'zaps':
+ list =
+ break
+ default:
+ break
+ }
return (
<>
-
-
-
+
- {type === 'replies' ? (
-
- ) : (
-
- )}
+ {list}
>
)
}
diff --git a/src/components/NoteStats/TopZaps.tsx b/src/components/NoteStats/TopZaps.tsx
index 6378fea..f3f2438 100644
--- a/src/components/NoteStats/TopZaps.tsx
+++ b/src/components/NoteStats/TopZaps.tsx
@@ -22,14 +22,14 @@ export default function TopZaps({ event }: { event: Event }) {
{topZaps.map((zap, index) => (
{
e.stopPropagation()
setZapIndex(index)
}}
>
-
+
{formatAmount(zap.amount)}
{zap.comment}
e.stopPropagation()}>
diff --git a/src/components/QuoteList/index.tsx b/src/components/QuoteList/index.tsx
index 79cc45a..37f6eec 100644
--- a/src/components/QuoteList/index.tsx
+++ b/src/components/QuoteList/index.tsx
@@ -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
{
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
return (
-
+
{events.slice(0, showCount).map((event) => {
if (hideUntrustedInteractions && !isUserTrusted(event.pubkey)) {
diff --git a/src/components/ReactionList/index.tsx b/src/components/ReactionList/index.tsx
new file mode 100644
index 0000000..4f8cae8
--- /dev/null
+++ b/src/components/ReactionList/index.tsx
@@ -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
(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 (
+
+ {filteredLikes.slice(0, showCount).map((like) => (
+
push(toProfile(like.pubkey))}
+ >
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+ {filteredLikes.length > 0 ? t('No more reactions') : t('No reactions yet')}
+
+
+ )
+}
diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx
index 3b8b67a..3fef286 100644
--- a/src/components/ReplyNoteList/index.tsx
+++ b/src/components/ReplyNoteList/index.tsx
@@ -274,7 +274,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
}, [])
return (
-
+
{loading && (replies.length === 0 ?
:
)}
{!loading && until && (
{
+ 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
(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 (
+
+ {filteredReposts.slice(0, showCount).map((repost) => (
+
push(toProfile(repost.pubkey))}
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+
+ {filteredReposts.length > 0 ? t('No more reposts') : t('No reposts yet')}
+
+
+ )
+}
diff --git a/src/components/ZapList/index.tsx b/src/components/ZapList/index.tsx
new file mode 100644
index 0000000..6a5eaca
--- /dev/null
+++ b/src/components/ZapList/index.tsx
@@ -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(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 (
+
+ {filteredZaps.slice(0, showCount).map((zap) => (
+
push(toProfile(zap.pubkey))}
+ >
+
+
+
{formatAmount(zap.amount)}
+
+
+
+
+ ))}
+
+
+
+
+ {filteredZaps.length > 0 ? t('No more zaps') : t('No zaps yet')}
+
+
+ )
+}
diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts
index 51062b1..2f25914 100644
--- a/src/i18n/locales/ar.ts
+++ b/src/i18n/locales/ar.ts
@@ -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: 'إعادة النشر'
}
}
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index 9b786fa..99acc0c 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -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'
}
}
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 12ae0e4..f48eb34 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -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'
}
}
diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts
index 9b07852..d5ac0c5 100644
--- a/src/i18n/locales/es.ts
+++ b/src/i18n/locales/es.ts
@@ -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'
}
}
diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts
index f5b292b..7b566b1 100644
--- a/src/i18n/locales/fa.ts
+++ b/src/i18n/locales/fa.ts
@@ -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: 'بازنشرها'
}
}
diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts
index a5f148e..3324f4d 100644
--- a/src/i18n/locales/fr.ts
+++ b/src/i18n/locales/fr.ts
@@ -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'
}
}
diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts
index 94aad99..2445ea7 100644
--- a/src/i18n/locales/it.ts
+++ b/src/i18n/locales/it.ts
@@ -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'
}
}
diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts
index f273923..68b5828 100644
--- a/src/i18n/locales/ja.ts
+++ b/src/i18n/locales/ja.ts
@@ -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: 'リポスト'
}
}
diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts
index 34123c5..4d52da4 100644
--- a/src/i18n/locales/ko.ts
+++ b/src/i18n/locales/ko.ts
@@ -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: '리포스트'
}
}
diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts
index 9021545..8fd9f9f 100644
--- a/src/i18n/locales/pl.ts
+++ b/src/i18n/locales/pl.ts
@@ -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'
}
}
diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts
index 6298e1a..9ba75b7 100644
--- a/src/i18n/locales/pt-BR.ts
+++ b/src/i18n/locales/pt-BR.ts
@@ -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'
}
}
diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts
index 1ad1c88..af28feb 100644
--- a/src/i18n/locales/pt-PT.ts
+++ b/src/i18n/locales/pt-PT.ts
@@ -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'
}
}
diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts
index 669a723..ec926ce 100644
--- a/src/i18n/locales/ru.ts
+++ b/src/i18n/locales/ru.ts
@@ -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: 'Репосты'
}
}
diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts
index 133a9fc..6e70de7 100644
--- a/src/i18n/locales/th.ts
+++ b/src/i18n/locales/th.ts
@@ -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: 'การรีโพสต์'
}
}
diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts
index caa2e9d..15e7514 100644
--- a/src/i18n/locales/zh.ts
+++ b/src/i18n/locales/zh.ts
@@ -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: '转发'
}
}
diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts
index 133ada9..23c9d1b 100644
--- a/src/lib/draft-event.ts
+++ b/src/lib/draft-event.ts
@@ -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 {
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 {
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(
isNsfw?: boolean
} = {}
): Promise {
- 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(
}
// 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(
protectedEvent?: boolean
} = {}
): Promise {
- 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(
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(
isNsfw?: boolean
} = {}
): Promise {
- 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(
isNsfw?: boolean
} = {}
): Promise {
- 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(
}
// 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[]) {
}
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) {
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) {
}
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) {
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) {
}
return {
- quoteEventIds,
+ quoteEventHexIds,
+ quoteReplaceableCoordinates,
rootEventId,
rootCoordinateTag,
rootKind,
@@ -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]
}
diff --git a/src/services/note-stats.service.ts b/src/services/note-stats.service.ts
index 6687bbd..27da4e4 100644
--- a/src/services/note-stats.service.ts
+++ b/src/services/note-stats.service.ts
@@ -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 = {
likeIdSet: Set
likes: { id: string; pubkey: string; created_at: number; emoji: TEmoji | string }[]
repostPubkeySet: Set
+ reposts: { id: string; pubkey: string; created_at: number }[]
zapPrSet: Set
- 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 {
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 {
}
]
+ 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 {
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 {
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 {
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 {
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 {
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
+ )
}
}