diff --git a/src/components/NoteCard/MainNoteCard.tsx b/src/components/NoteCard/MainNoteCard.tsx
index a2b089a..6c3159d 100644
--- a/src/components/NoteCard/MainNoteCard.tsx
+++ b/src/components/NoteCard/MainNoteCard.tsx
@@ -12,14 +12,14 @@ import RepostDescription from './RepostDescription'
export default function MainNoteCard({
event,
className,
- reposter,
+ reposters,
embedded,
originalNoteId,
pinned = false
}: {
event: Event
className?: string
- reposter?: string
+ reposters?: string[]
embedded?: boolean
originalNoteId?: string
pinned?: boolean
@@ -37,7 +37,7 @@ export default function MainNoteCard({
{pinned && }
-
+
3: show "Alice, Bob, and x others reposted" (with hover card showing avatars of others)
+ */
export default function RepostDescription({
- reposter,
+ reposters,
className
}: {
- reposter?: string | null
+ reposters?: string[]
className?: string
}) {
const { t } = useTranslation()
- if (!reposter) return null
+ if (!reposters?.length) return null
return (
-
+
1 && 'after:content-[","]')}
+ skeletonClassName="h-3"
+ />
+ {reposters.length > 1 && (
+
+ )}
+ {reposters.length > 3 ? (
+
+ ) : reposters.length === 3 ? (
+
+ ) : null}
{t('reposted')}
)
}
+
+function AndXOthers({ reposters }: { reposters: string[] }) {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+ {t('and {{x}} others', { x: reposters.length })}
+
+
+
+ {reposters.map((pubkey) => (
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/NoteCard/RepostNoteCard.tsx b/src/components/NoteCard/RepostNoteCard.tsx
index 5cd562c..0dfd636 100644
--- a/src/components/NoteCard/RepostNoteCard.tsx
+++ b/src/components/NoteCard/RepostNoteCard.tsx
@@ -1,9 +1,9 @@
import { isMentioningMutedUsers } from '@/lib/event'
-import { tagNameEquals } from '@/lib/tag'
+import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import client from '@/services/client.service'
-import { Event, kinds, nip19, verifyEvent } from 'nostr-tools'
+import { Event, kinds, verifyEvent } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react'
import MainNoteCard from './MainNoteCard'
@@ -51,15 +51,20 @@ export default function RepostNoteCard({
return
}
- const [, id, relay, , pubkey] = event.tags.find(tagNameEquals('e')) ?? []
- if (!id) {
+ let targetEventId: string | undefined
+ const aTag = event.tags.find(tagNameEquals('a'))
+ if (aTag) {
+ targetEventId = generateBech32IdFromATag(aTag)
+ } else {
+ const eTag = event.tags.find(tagNameEquals('e'))
+ if (eTag) {
+ targetEventId = generateBech32IdFromETag(eTag)
+ }
+ }
+ if (!targetEventId) {
return
}
- const targetEventId = nip19.neventEncode({
- id,
- relays: relay ? [relay] : [],
- author: pubkey
- })
+
const targetEvent = await client.fetchEvent(targetEventId)
if (targetEvent) {
setTargetEvent(targetEvent)
@@ -76,7 +81,7 @@ export default function RepostNoteCard({
return (
diff --git a/src/components/NoteCard/index.tsx b/src/components/NoteCard/index.tsx
index 5dd8c32..b095315 100644
--- a/src/components/NoteCard/index.tsx
+++ b/src/components/NoteCard/index.tsx
@@ -11,12 +11,14 @@ export default function NoteCard({
event,
className,
filterMutedNotes = true,
- pinned = false
+ pinned = false,
+ reposters
}: {
event: Event
className?: string
filterMutedNotes?: boolean
pinned?: boolean
+ reposters?: string[]
}) {
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
@@ -41,7 +43,7 @@ export default function NoteCard({
/>
)
}
- return
+ return
}
export function NoteCardLoadingSkeleton() {
diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx
index 4378f41..4fb2ba7 100644
--- a/src/components/NoteList/index.tsx
+++ b/src/components/NoteList/index.tsx
@@ -1,11 +1,12 @@
import NewNotesButton from '@/components/NewNotesButton'
import { Button } from '@/components/ui/button'
import {
- getReplaceableCoordinateFromEvent,
+ getEventKey,
+ getEventKeyFromTag,
isMentioningMutedUsers,
- isReplaceableEvent,
isReplyNoteEvent
} from '@/lib/event'
+import { tagNameEquals } from '@/lib/tag'
import { isTouchDevice } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider'
@@ -15,7 +16,7 @@ import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types'
import dayjs from 'dayjs'
-import { Event } from 'nostr-tools'
+import { Event, kinds, verifyEvent } from 'nostr-tools'
import { decode } from 'nostr-tools/nip19'
import {
forwardRef,
@@ -113,34 +114,95 @@ const NoteList = forwardRef(
[hideReplies, hideUntrustedNotes, mutePubkeySet, pinnedEventIds, isEventDeleted, filterFn]
)
- const filteredEvents = useMemo(() => {
- const idSet = new Set()
+ const filteredNotes = useMemo(() => {
+ // Store processed event keys to avoid duplicates
+ const keySet = new Set()
+ // Map to track reposters for each event key
+ const repostersMap = new Map>()
+ // Final list of filtered events
+ const filteredEvents: Event[] = []
- return events.slice(0, showCount).filter((evt) => {
- if (shouldHideEvent(evt)) return false
+ events.slice(0, showCount).forEach((evt) => {
+ const key = getEventKey(evt)
+ if (keySet.has(key)) return
+ keySet.add(key)
- const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id
- if (idSet.has(id)) {
- return false
+ if (shouldHideEvent(evt)) return
+ if (evt.kind !== kinds.Repost) {
+ filteredEvents.push(evt)
+ return
}
- idSet.add(id)
- return true
+
+ const eventFromContent = evt.content ? (JSON.parse(evt.content) as Event) : null
+ if (eventFromContent && verifyEvent(eventFromContent)) {
+ if (eventFromContent.kind === kinds.Repost) {
+ return
+ }
+ if (shouldHideEvent(eventFromContent)) return
+
+ client.addEventToCache(eventFromContent)
+ const targetSeenOn = client.getSeenEventRelays(eventFromContent.id)
+ if (targetSeenOn.length === 0) {
+ const seenOn = client.getSeenEventRelays(evt.id)
+ seenOn.forEach((relay) => {
+ client.trackEventSeenOn(eventFromContent.id, relay)
+ })
+ }
+
+ const targetEventKey = getEventKey(eventFromContent)
+ const reposters = repostersMap.get(targetEventKey)
+ if (reposters) {
+ reposters.add(evt.pubkey)
+ } else {
+ repostersMap.set(targetEventKey, new Set([evt.pubkey]))
+ }
+ // If the target event is not already included, add it now
+ if (!keySet.has(targetEventKey)) {
+ filteredEvents.push(eventFromContent)
+ keySet.add(targetEventKey)
+ }
+ return
+ }
+
+ const targetTag = evt.tags.find(tagNameEquals('a')) ?? evt.tags.find(tagNameEquals('e'))
+ if (targetTag) {
+ const targetEventKey = getEventKeyFromTag(targetTag)
+ if (targetEventKey) {
+ // Add to reposters map
+ const reposters = repostersMap.get(targetEventKey)
+ if (reposters) {
+ reposters.add(evt.pubkey)
+ } else {
+ repostersMap.set(targetEventKey, new Set([evt.pubkey]))
+ }
+ // If the target event is already included, skip adding this repost
+ if (keySet.has(targetEventKey)) {
+ return
+ }
+ }
+ }
+ // If we can't find the original event, just show the repost itself
+ filteredEvents.push(evt)
+ return
+ })
+
+ return filteredEvents.map((evt) => {
+ const key = getEventKey(evt)
+ return { key, event: evt, reposters: Array.from(repostersMap.get(key) ?? []) }
})
}, [events, showCount, shouldHideEvent])
const filteredNewEvents = useMemo(() => {
- const idSet = new Set()
+ const keySet = new Set()
return newEvents.filter((event: Event) => {
if (shouldHideEvent(event)) return false
- const id = isReplaceableEvent(event.kind)
- ? getReplaceableCoordinateFromEvent(event)
- : event.id
- if (idSet.has(id)) {
+ const key = getEventKey(event)
+ if (keySet.has(key)) {
return false
}
- idSet.add(id)
+ keySet.add(key)
return true
})
}, [newEvents, shouldHideEvent])
@@ -306,12 +368,13 @@ const NoteList = forwardRef(
{pinnedEventIds.map((id) => (
))}
- {filteredEvents.map((event) => (
+ {filteredNotes.map(({ key, event, reposters }) => (
))}
{hasMore || loading ? (
diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts
index a185ab9..adbf8e7 100644
--- a/src/i18n/locales/ar.ts
+++ b/src/i18n/locales/ar.ts
@@ -489,6 +489,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble هو عميل يركز على تصفح المرحلات. ابدأ باستكشاف المرحلات المثيرة للاهتمام أو قم بتسجيل الدخول لعرض خلاصتك.',
'Explore Relays': 'استكشف المرحلات',
- 'Choose a feed': 'اختر خلاصة'
+ 'Choose a feed': 'اختر خلاصة',
+ 'and {{x}} others': 'و {{x}} آخرون'
}
}
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index 2a879f5..391e676 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -503,6 +503,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble ist ein Client, der sich auf das Durchsuchen von Relays konzentriert. Beginnen Sie mit der Erkundung interessanter Relays oder melden Sie sich an, um Ihren Following-Feed anzuzeigen.',
'Explore Relays': 'Relays erkunden',
- 'Choose a feed': 'Wähle einen Feed'
+ 'Choose a feed': 'Wähle einen Feed',
+ 'and {{x}} others': 'und {{x}} andere'
}
}
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 9afbfa1..2745f98 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -488,6 +488,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.',
'Explore Relays': 'Explore Relays',
- 'Choose a feed': 'Choose a feed'
+ 'Choose a feed': 'Choose a feed',
+ 'and {{x}} others': 'and {{x}} others'
}
}
diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts
index 57ae48b..71d6560 100644
--- a/src/i18n/locales/es.ts
+++ b/src/i18n/locales/es.ts
@@ -497,6 +497,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble es un cliente enfocado en explorar relays. Comienza explorando relays interesantes o inicia sesión para ver tu feed de seguidos.',
'Explore Relays': 'Explorar Relays',
- 'Choose a feed': 'Elige un feed'
+ 'Choose a feed': 'Elige un feed',
+ 'and {{x}} others': 'y {{x}} otros'
}
}
diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts
index 68139be..8202d70 100644
--- a/src/i18n/locales/fa.ts
+++ b/src/i18n/locales/fa.ts
@@ -492,6 +492,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble یک کلاینت متمرکز بر مرور رلههاست. با کاوش در رلههای جالب شروع کنید یا وارد شوید تا فید دنبالکنندههای خود را مشاهده کنید.',
'Explore Relays': 'کاوش در رلهها',
- 'Choose a feed': 'یک فید انتخاب کنید'
+ 'Choose a feed': 'یک فید انتخاب کنید',
+ 'and {{x}} others': 'و {{x}} دیگر'
}
}
diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts
index a681fb6..c3a662e 100644
--- a/src/i18n/locales/fr.ts
+++ b/src/i18n/locales/fr.ts
@@ -502,6 +502,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
"Jumble est un client axé sur la navigation des relais. Commencez par explorer des relais intéressants ou connectez-vous pour voir votre fil d'abonnements.",
'Explore Relays': 'Explorer les relais',
- 'Choose a feed': 'Choisir un fil'
+ 'Choose a feed': 'Choisir un fil',
+ 'and {{x}} others': 'et {{x}} autres'
}
}
diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts
index 242d8f5..1bfe96c 100644
--- a/src/i18n/locales/hi.ts
+++ b/src/i18n/locales/hi.ts
@@ -494,6 +494,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble एक क्लाइंट है जो रिले ब्राउज़ करने पर केंद्रित है। रोचक रिले की खोज करके शुरू करें या अपनी फ़ॉलोइंग फ़ीड देखने के लिए लॉगिन करें।',
'Explore Relays': 'रिले एक्सप्लोर करें',
- 'Choose a feed': 'एक फीड चुनें'
+ 'Choose a feed': 'एक फीड चुनें',
+ 'and {{x}} others': 'और {{x}} अन्य'
}
}
diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts
index 0247b84..aeb596c 100644
--- a/src/i18n/locales/it.ts
+++ b/src/i18n/locales/it.ts
@@ -497,6 +497,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble è un client focalizzato sulla navigazione dei relay. Inizia esplorando relay interessanti o effettua il login per visualizzare il tuo feed di following.',
'Explore Relays': 'Esplora Relay',
- 'Choose a feed': 'Scegli un feed'
+ 'Choose a feed': 'Scegli un feed',
+ 'and {{x}} others': 'e altri {{x}}'
}
}
diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts
index 453e9ca..a717e15 100644
--- a/src/i18n/locales/ja.ts
+++ b/src/i18n/locales/ja.ts
@@ -493,6 +493,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumbleはリレーを閲覧することに焦点を当てたクライアントです。興味深いリレーを探索するか、ログインしてフォロー中のフィードを表示してください。',
'Explore Relays': 'リレーを探索',
- 'Choose a feed': 'フィードを選択'
+ 'Choose a feed': 'フィードを選択',
+ 'and {{x}} others': 'および他{{x}}人'
}
}
diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts
index 62d4822..fa23798 100644
--- a/src/i18n/locales/ko.ts
+++ b/src/i18n/locales/ko.ts
@@ -493,6 +493,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble은 릴레이 탐색에 중점을 둔 클라이언트입니다. 흥미로운 릴레이를 탐색하거나 로그인하여 팔로잉 피드를 확인하세요.',
'Explore Relays': '릴레이 탐색',
- 'Choose a feed': '피드 선택'
+ 'Choose a feed': '피드 선택',
+ 'and {{x}} others': '및 기타 {{x}}명'
}
}
diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts
index fe29e49..652c55f 100644
--- a/src/i18n/locales/pl.ts
+++ b/src/i18n/locales/pl.ts
@@ -497,6 +497,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble to klient skupiony na przeglądaniu relay. Zacznij od eksploracji ciekawych relay lub zaloguj się, aby zobaczyć swój feed obserwowanych.',
'Explore Relays': 'Eksploruj Relay',
- 'Choose a feed': 'Wybierz feed'
+ 'Choose a feed': 'Wybierz feed',
+ 'and {{x}} others': 'i {{x}} innych'
}
}
diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts
index b3533a3..10cfeee 100644
--- a/src/i18n/locales/pt-BR.ts
+++ b/src/i18n/locales/pt-BR.ts
@@ -494,6 +494,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble é um cliente focado em navegar relays. Comece explorando relays interessantes ou faça login para ver seu feed de seguidos.',
'Explore Relays': 'Explorar Relays',
- 'Choose a feed': 'Escolha um feed'
+ 'Choose a feed': 'Escolha um feed',
+ 'and {{x}} others': 'e {{x}} outros'
}
}
diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts
index 016fb8b..b6ce7a3 100644
--- a/src/i18n/locales/pt-PT.ts
+++ b/src/i18n/locales/pt-PT.ts
@@ -497,6 +497,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble é um cliente focado em explorar relays. Comece por explorar relays interessantes ou inicie sessão para ver o seu feed de seguidos.',
'Explore Relays': 'Explorar Relays',
- 'Choose a feed': 'Escolha um feed'
+ 'Choose a feed': 'Escolha um feed',
+ 'and {{x}} others': 'e {{x}} outros'
}
}
diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts
index 3893505..ea719b2 100644
--- a/src/i18n/locales/ru.ts
+++ b/src/i18n/locales/ru.ts
@@ -499,6 +499,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble — это клиент, ориентированный на просмотр relay. Начните с изучения интересных relay или войдите, чтобы увидеть ленту подписок.',
'Explore Relays': 'Исследовать Relay',
- 'Choose a feed': 'Выберите ленту'
+ 'Choose a feed': 'Выберите ленту',
+ 'and {{x}} others': 'и {{x}} других'
}
}
diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts
index a314eba..fa5a958 100644
--- a/src/i18n/locales/th.ts
+++ b/src/i18n/locales/th.ts
@@ -487,6 +487,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble เป็นไคลเอนต์ที่เน้นการเรียกดูรีเลย์ เริ่มต้นด้วยการสำรวจรีเลย์ที่น่าสนใจ หรือเข้าสู่ระบบเพื่อดูฟีดที่คุณติดตาม',
'Explore Relays': 'สำรวจรีเลย์',
- 'Choose a feed': 'เลือกฟีด'
+ 'Choose a feed': 'เลือกฟีด',
+ 'and {{x}} others': 'และอื่น ๆ {{x}} รายการ'
}
}
diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts
index acaa789..83b6f99 100644
--- a/src/i18n/locales/zh.ts
+++ b/src/i18n/locales/zh.ts
@@ -485,6 +485,7 @@ export default {
'Jumble is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.':
'Jumble 是一个专注于浏览服务器的客户端。从探索有趣的服务器开始,或者登录查看你的关注动态。',
'Explore Relays': '探索服务器',
- 'Choose a feed': '选择一个动态'
+ 'Choose a feed': '选择一个动态',
+ 'and {{x}} others': '和其他 {{x}} 人'
}
}