feat: support for generic repost
This commit is contained in:
@@ -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' },
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -88,6 +88,7 @@ export const ExtendedKind = {
|
||||
export const SUPPORTED_KINDS = [
|
||||
kinds.ShortTextNote,
|
||||
kinds.Repost,
|
||||
kinds.GenericRepost,
|
||||
ExtendedKind.PICTURE,
|
||||
ExtendedKind.VIDEO,
|
||||
ExtendedKind.SHORT_VIDEO,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user