feat: support for generic repost

This commit is contained in:
codytseng
2025-11-20 13:34:05 +08:00
parent 14b3fbd496
commit a40d7b0676
11 changed files with 92 additions and 68 deletions

View File

@@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next'
const KIND_FILTER_OPTIONS = [
{ kindGroup: [kinds.ShortTextNote, ExtendedKind.COMMENT], label: 'Posts' },
{ kindGroup: [kinds.Repost], label: 'Reposts' },
{ kindGroup: [kinds.Repost, kinds.GenericRepost], label: 'Reposts' },
{ kindGroup: [kinds.LongFormArticle], label: 'Articles' },
{ kindGroup: [kinds.Highlights], label: 'Highlights' },
{ kindGroup: [ExtendedKind.POLL], label: 'Polls' },

View File

@@ -11,12 +11,14 @@ export default function RepostNoteCard({
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()
@@ -42,7 +44,10 @@ export default function RepostNoteCard({
}
}
if (eventFromContent && verifyEvent(eventFromContent)) {
if (eventFromContent.kind === kinds.Repost) {
if (
eventFromContent.kind === kinds.Repost ||
eventFromContent.kind === kinds.GenericRepost
) {
return
}
client.addEventToCache(eventFromContent)
@@ -84,7 +89,7 @@ export default function RepostNoteCard({
return (
<MainNoteCard
className={className}
reposters={[event.pubkey]}
reposters={reposters?.includes(event.pubkey) ? reposters : [event.pubkey]}
event={targetEvent}
pinned={pinned}
/>

View File

@@ -34,13 +34,14 @@ export default function NoteCard({
}, [event, filterMutedNotes, mutePubkeySet])
if (shouldHide) return null
if (event.kind === kinds.Repost) {
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) {
return (
<RepostNoteCard
event={event}
className={className}
filterMutedNotes={filterMutedNotes}
pinned={pinned}
reposters={reposters}
/>
)
}

View File

@@ -11,7 +11,7 @@ import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types'
import dayjs from 'dayjs'
import { Event, kinds, verifyEvent } from 'nostr-tools'
import { Event, kinds } from 'nostr-tools'
import { decode } from 'nostr-tools/nip19'
import {
forwardRef,
@@ -117,84 +117,75 @@ const NoteList = forwardRef(
const repostersMap = new Map<string, Set<string>>()
// Final list of filtered events
const filteredEvents: Event[] = []
const keys: string[] = []
events.slice(0, showCount).forEach((evt) => {
events.forEach((evt) => {
const key = getEventKey(evt)
if (keySet.has(key)) return
keySet.add(key)
if (shouldHideEvent(evt)) return
if (hideReplies && isReplyNoteEvent(evt)) return
if (evt.kind !== kinds.Repost) {
if (evt.kind !== kinds.Repost && evt.kind !== kinds.GenericRepost) {
filteredEvents.push(evt)
keys.push(key)
return
}
let targetEventKey: string | undefined
let eventFromContent: Event | null = null
if (evt.content) {
try {
eventFromContent = JSON.parse(evt.content) as Event
} catch {
eventFromContent = null
const targetTag = evt.tags.find(tagNameEquals('a')) ?? evt.tags.find(tagNameEquals('e'))
if (targetTag) {
targetEventKey = getKeyFromTag(targetTag)
} else {
// Attempt to extract the target event from the repost content
if (evt.content) {
try {
eventFromContent = JSON.parse(evt.content) as Event
} catch {
eventFromContent = null
}
}
if (eventFromContent) {
if (
eventFromContent.kind === kinds.Repost ||
eventFromContent.kind === kinds.GenericRepost
) {
return
}
if (shouldHideEvent(evt)) return
targetEventKey = getEventKey(eventFromContent)
}
}
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)
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 not already included, add it now
if (!keySet.has(targetEventKey)) {
filteredEvents.push(eventFromContent)
filteredEvents.push(evt)
keys.push(targetEventKey)
keySet.add(targetEventKey)
}
return
}
const targetTag = evt.tags.find(tagNameEquals('a')) ?? evt.tags.find(tagNameEquals('e'))
if (targetTag) {
const targetEventKey = getKeyFromTag(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 filteredEvents.map((evt, i) => {
const key = keys[i]
return { key, event: evt, reposters: Array.from(repostersMap.get(key) ?? []) }
})
}, [events, showCount, shouldHideEvent, hideReplies])
}, [events, shouldHideEvent, hideReplies])
const slicedNotes = useMemo(() => {
return filteredNotes.slice(0, showCount)
}, [filteredNotes, showCount])
const filteredNewEvents = useMemo(() => {
const keySet = new Set<string>()
@@ -369,7 +360,7 @@ const NoteList = forwardRef(
const list = (
<div className="min-h-screen">
{pinnedEventIds?.map((id) => <PinnedNoteCard key={id} eventId={id} className="w-full" />)}
{filteredNotes.map(({ key, event, reposters }) => (
{slicedNotes.map(({ key, event, reposters }) => (
<NoteCard
key={key}
className="w-full"

View File

@@ -51,7 +51,7 @@ export function NotificationItem({
) {
return <MentionNotification notification={notification} isNew={isNew} />
}
if (notification.kind === kinds.Repost) {
if (notification.kind === kinds.Repost || notification.kind === kinds.GenericRepost) {
return <RepostNotification notification={notification} isNew={isNew} />
}
if (notification.kind === kinds.Zap) {

View File

@@ -58,13 +58,14 @@ const NotificationList = forwardRef((_, ref) => {
ExtendedKind.POLL
]
case 'reactions':
return [kinds.Reaction, kinds.Repost, ExtendedKind.POLL_RESPONSE]
return [kinds.Reaction, kinds.Repost, kinds.GenericRepost, ExtendedKind.POLL_RESPONSE]
case 'zaps':
return [kinds.Zap]
default:
return [
kinds.ShortTextNote,
kinds.Repost,
kinds.GenericRepost,
kinds.Reaction,
kinds.Zap,
ExtendedKind.COMMENT,

View File

@@ -1,5 +1,6 @@
import { useSecondaryPage } from '@/PageManager'
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
import { getEventKey } from '@/lib/event'
import { toProfile } from '@/lib/link'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
@@ -19,7 +20,7 @@ export default function RepostList({ event }: { event: Event }) {
const { push } = useSecondaryPage()
const { isSmallScreen } = useScreenSize()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const noteStats = useStuffStatsById(event.id)
const noteStats = useStuffStatsById(getEventKey(event))
const filteredReposts = useMemo(() => {
return (noteStats?.reposts ?? [])
.filter((repost) => !hideUntrustedInteractions || isUserTrusted(repost.pubkey))

View File

@@ -88,6 +88,7 @@ export const ExtendedKind = {
export const SUPPORTED_KINDS = [
kinds.ShortTextNote,
kinds.Repost,
kinds.GenericRepost,
ExtendedKind.PICTURE,
ExtendedKind.VIDEO,
ExtendedKind.SHORT_VIDEO,

View File

@@ -118,12 +118,22 @@ export function createRepostDraftEvent(event: Event): TDraftEvent {
const isProtected = isProtectedEvent(event)
const tags = [buildETag(event.id, event.pubkey), buildPTag(event.pubkey)]
if (event.kind === kinds.ShortTextNote) {
return {
kind: kinds.Repost,
content: isProtected ? '' : JSON.stringify(event),
tags,
created_at: dayjs().unix()
}
}
tags.push(buildKTag(event.kind))
if (isReplaceableEvent(event.kind)) {
tags.push(buildATag(event))
}
return {
kind: kinds.Repost,
kind: kinds.GenericRepost,
content: isProtected ? '' : JSON.stringify(event),
tags,
created_at: dayjs().unix()

View File

@@ -23,6 +23,7 @@ import {
TThemeSetting,
TTranslationServiceConfig
} from '@/types'
import { kinds } from 'nostr-tools'
class LocalStorageService {
static instance: LocalStorageService
@@ -181,10 +182,13 @@ class LocalStorageService {
showKindSet.delete(24236) // remove typo kind
showKindSet.add(ExtendedKind.ADDRESSABLE_SHORT_VIDEO)
}
if (showKindsVersion < 4 && showKindSet.has(kinds.Repost)) {
showKindSet.add(kinds.GenericRepost)
}
this.showKinds = Array.from(showKindSet)
}
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(this.showKinds))
window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '3')
window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '4')
this.hideContentMentioningMutedUsers =
window.localStorage.getItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'

View File

@@ -107,7 +107,10 @@ class StuffStatsService {
? {
'#e': [event.id],
authors: [pubkey],
kinds: [kinds.Reaction, kinds.Repost]
kinds:
event.kind === kinds.ShortTextNote
? [kinds.Reaction, kinds.Repost]
: [kinds.Reaction, kinds.Repost, kinds.GenericRepost]
}
: {
'#i': [externalContent],
@@ -120,7 +123,7 @@ class StuffStatsService {
filters.push({
'#a': [replaceableCoordinate],
authors: [pubkey],
kinds: [kinds.Reaction, kinds.Repost]
kinds: [kinds.Reaction, kinds.Repost, kinds.GenericRepost]
})
}
@@ -218,7 +221,7 @@ class StuffStatsService {
targetKey = this.addLikeByEvent(evt)
} else if (evt.kind === ExtendedKind.EXTERNAL_CONTENT_REACTION) {
targetKey = this.addExternalContentLikeByEvent(evt)
} else if (evt.kind === kinds.Repost) {
} else if (evt.kind === kinds.Repost || evt.kind === kinds.GenericRepost) {
targetKey = this.addRepostByEvent(evt)
} else if (evt.kind === kinds.Zap) {
targetKey = this.addZapByEvent(evt)
@@ -291,18 +294,25 @@ class StuffStatsService {
}
private addRepostByEvent(evt: Event) {
const eventId = evt.tags.find(tagNameEquals('e'))?.[1]
if (!eventId) return
let targetEventKey
targetEventKey = evt.tags.find(tagNameEquals('a'))?.[1]
if (!targetEventKey) {
targetEventKey = evt.tags.find(tagNameEquals('e'))?.[1]
}
const old = this.stuffStatsMap.get(eventId) || {}
if (!targetEventKey) {
return
}
const old = this.stuffStatsMap.get(targetEventKey) || {}
const repostPubkeySet = old.repostPubkeySet || new Set()
const reposts = old.reposts || []
if (repostPubkeySet.has(evt.pubkey)) return
repostPubkeySet.add(evt.pubkey)
reposts.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
this.stuffStatsMap.set(eventId, { ...old, repostPubkeySet, reposts })
return eventId
this.stuffStatsMap.set(targetEventKey, { ...old, repostPubkeySet, reposts })
return targetEventKey
}
private addZapByEvent(evt: Event) {