feat: support for generic repost
This commit is contained in:
@@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
const KIND_FILTER_OPTIONS = [
|
const KIND_FILTER_OPTIONS = [
|
||||||
{ kindGroup: [kinds.ShortTextNote, ExtendedKind.COMMENT], label: 'Posts' },
|
{ 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.LongFormArticle], label: 'Articles' },
|
||||||
{ kindGroup: [kinds.Highlights], label: 'Highlights' },
|
{ kindGroup: [kinds.Highlights], label: 'Highlights' },
|
||||||
{ kindGroup: [ExtendedKind.POLL], label: 'Polls' },
|
{ kindGroup: [ExtendedKind.POLL], label: 'Polls' },
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ export default function RepostNoteCard({
|
|||||||
event,
|
event,
|
||||||
className,
|
className,
|
||||||
filterMutedNotes = true,
|
filterMutedNotes = true,
|
||||||
pinned = false
|
pinned = false,
|
||||||
|
reposters
|
||||||
}: {
|
}: {
|
||||||
event: Event
|
event: Event
|
||||||
className?: string
|
className?: string
|
||||||
filterMutedNotes?: boolean
|
filterMutedNotes?: boolean
|
||||||
pinned?: boolean
|
pinned?: boolean
|
||||||
|
reposters?: string[]
|
||||||
}) {
|
}) {
|
||||||
const { mutePubkeySet } = useMuteList()
|
const { mutePubkeySet } = useMuteList()
|
||||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||||
@@ -42,7 +44,10 @@ export default function RepostNoteCard({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (eventFromContent && verifyEvent(eventFromContent)) {
|
if (eventFromContent && verifyEvent(eventFromContent)) {
|
||||||
if (eventFromContent.kind === kinds.Repost) {
|
if (
|
||||||
|
eventFromContent.kind === kinds.Repost ||
|
||||||
|
eventFromContent.kind === kinds.GenericRepost
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
client.addEventToCache(eventFromContent)
|
client.addEventToCache(eventFromContent)
|
||||||
@@ -84,7 +89,7 @@ export default function RepostNoteCard({
|
|||||||
return (
|
return (
|
||||||
<MainNoteCard
|
<MainNoteCard
|
||||||
className={className}
|
className={className}
|
||||||
reposters={[event.pubkey]}
|
reposters={reposters?.includes(event.pubkey) ? reposters : [event.pubkey]}
|
||||||
event={targetEvent}
|
event={targetEvent}
|
||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -34,13 +34,14 @@ export default function NoteCard({
|
|||||||
}, [event, filterMutedNotes, mutePubkeySet])
|
}, [event, filterMutedNotes, mutePubkeySet])
|
||||||
if (shouldHide) return null
|
if (shouldHide) return null
|
||||||
|
|
||||||
if (event.kind === kinds.Repost) {
|
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) {
|
||||||
return (
|
return (
|
||||||
<RepostNoteCard
|
<RepostNoteCard
|
||||||
event={event}
|
event={event}
|
||||||
className={className}
|
className={className}
|
||||||
filterMutedNotes={filterMutedNotes}
|
filterMutedNotes={filterMutedNotes}
|
||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
|
reposters={reposters}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useUserTrust } from '@/providers/UserTrustProvider'
|
|||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import { TFeedSubRequest } from '@/types'
|
import { TFeedSubRequest } from '@/types'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { decode } from 'nostr-tools/nip19'
|
import { decode } from 'nostr-tools/nip19'
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@@ -117,20 +117,28 @@ const NoteList = forwardRef(
|
|||||||
const repostersMap = new Map<string, Set<string>>()
|
const repostersMap = new Map<string, Set<string>>()
|
||||||
// Final list of filtered events
|
// Final list of filtered events
|
||||||
const filteredEvents: Event[] = []
|
const filteredEvents: Event[] = []
|
||||||
|
const keys: string[] = []
|
||||||
|
|
||||||
events.slice(0, showCount).forEach((evt) => {
|
events.forEach((evt) => {
|
||||||
const key = getEventKey(evt)
|
const key = getEventKey(evt)
|
||||||
if (keySet.has(key)) return
|
if (keySet.has(key)) return
|
||||||
keySet.add(key)
|
keySet.add(key)
|
||||||
|
|
||||||
if (shouldHideEvent(evt)) return
|
if (shouldHideEvent(evt)) return
|
||||||
if (hideReplies && isReplyNoteEvent(evt)) return
|
if (hideReplies && isReplyNoteEvent(evt)) return
|
||||||
if (evt.kind !== kinds.Repost) {
|
if (evt.kind !== kinds.Repost && evt.kind !== kinds.GenericRepost) {
|
||||||
filteredEvents.push(evt)
|
filteredEvents.push(evt)
|
||||||
|
keys.push(key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let targetEventKey: string | undefined
|
||||||
let eventFromContent: Event | null = null
|
let eventFromContent: Event | null = 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) {
|
if (evt.content) {
|
||||||
try {
|
try {
|
||||||
eventFromContent = JSON.parse(evt.content) as Event
|
eventFromContent = JSON.parse(evt.content) as Event
|
||||||
@@ -138,39 +146,19 @@ const NoteList = forwardRef(
|
|||||||
eventFromContent = null
|
eventFromContent = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (eventFromContent && verifyEvent(eventFromContent)) {
|
if (eventFromContent) {
|
||||||
if (eventFromContent.kind === kinds.Repost) {
|
if (
|
||||||
|
eventFromContent.kind === kinds.Repost ||
|
||||||
|
eventFromContent.kind === kinds.GenericRepost
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (shouldHideEvent(eventFromContent)) return
|
if (shouldHideEvent(evt)) return
|
||||||
|
|
||||||
client.addEventToCache(eventFromContent)
|
targetEventKey = getEventKey(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 = getKeyFromTag(targetTag)
|
|
||||||
if (targetEventKey) {
|
if (targetEventKey) {
|
||||||
// Add to reposters map
|
// Add to reposters map
|
||||||
const reposters = repostersMap.get(targetEventKey)
|
const reposters = repostersMap.get(targetEventKey)
|
||||||
@@ -179,22 +167,25 @@ const NoteList = forwardRef(
|
|||||||
} else {
|
} else {
|
||||||
repostersMap.set(targetEventKey, new Set([evt.pubkey]))
|
repostersMap.set(targetEventKey, new Set([evt.pubkey]))
|
||||||
}
|
}
|
||||||
// If the target event is already included, skip adding this repost
|
|
||||||
if (keySet.has(targetEventKey)) {
|
// If the target event is not already included, add it now
|
||||||
return
|
if (!keySet.has(targetEventKey)) {
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we can't find the original event, just show the repost itself
|
|
||||||
filteredEvents.push(evt)
|
filteredEvents.push(evt)
|
||||||
return
|
keys.push(targetEventKey)
|
||||||
|
keySet.add(targetEventKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return filteredEvents.map((evt) => {
|
return filteredEvents.map((evt, i) => {
|
||||||
const key = getEventKey(evt)
|
const key = keys[i]
|
||||||
return { key, event: evt, reposters: Array.from(repostersMap.get(key) ?? []) }
|
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 filteredNewEvents = useMemo(() => {
|
||||||
const keySet = new Set<string>()
|
const keySet = new Set<string>()
|
||||||
@@ -369,7 +360,7 @@ const NoteList = forwardRef(
|
|||||||
const list = (
|
const list = (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
{pinnedEventIds?.map((id) => <PinnedNoteCard key={id} eventId={id} className="w-full" />)}
|
{pinnedEventIds?.map((id) => <PinnedNoteCard key={id} eventId={id} className="w-full" />)}
|
||||||
{filteredNotes.map(({ key, event, reposters }) => (
|
{slicedNotes.map(({ key, event, reposters }) => (
|
||||||
<NoteCard
|
<NoteCard
|
||||||
key={key}
|
key={key}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function NotificationItem({
|
|||||||
) {
|
) {
|
||||||
return <MentionNotification notification={notification} isNew={isNew} />
|
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} />
|
return <RepostNotification notification={notification} isNew={isNew} />
|
||||||
}
|
}
|
||||||
if (notification.kind === kinds.Zap) {
|
if (notification.kind === kinds.Zap) {
|
||||||
|
|||||||
@@ -58,13 +58,14 @@ const NotificationList = forwardRef((_, ref) => {
|
|||||||
ExtendedKind.POLL
|
ExtendedKind.POLL
|
||||||
]
|
]
|
||||||
case 'reactions':
|
case 'reactions':
|
||||||
return [kinds.Reaction, kinds.Repost, ExtendedKind.POLL_RESPONSE]
|
return [kinds.Reaction, kinds.Repost, kinds.GenericRepost, ExtendedKind.POLL_RESPONSE]
|
||||||
case 'zaps':
|
case 'zaps':
|
||||||
return [kinds.Zap]
|
return [kinds.Zap]
|
||||||
default:
|
default:
|
||||||
return [
|
return [
|
||||||
kinds.ShortTextNote,
|
kinds.ShortTextNote,
|
||||||
kinds.Repost,
|
kinds.Repost,
|
||||||
|
kinds.GenericRepost,
|
||||||
kinds.Reaction,
|
kinds.Reaction,
|
||||||
kinds.Zap,
|
kinds.Zap,
|
||||||
ExtendedKind.COMMENT,
|
ExtendedKind.COMMENT,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useSecondaryPage } from '@/PageManager'
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
|
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
|
||||||
|
import { getEventKey } from '@/lib/event'
|
||||||
import { toProfile } from '@/lib/link'
|
import { toProfile } from '@/lib/link'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||||
@@ -19,7 +20,7 @@ export default function RepostList({ event }: { event: Event }) {
|
|||||||
const { push } = useSecondaryPage()
|
const { push } = useSecondaryPage()
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||||
const noteStats = useStuffStatsById(event.id)
|
const noteStats = useStuffStatsById(getEventKey(event))
|
||||||
const filteredReposts = useMemo(() => {
|
const filteredReposts = useMemo(() => {
|
||||||
return (noteStats?.reposts ?? [])
|
return (noteStats?.reposts ?? [])
|
||||||
.filter((repost) => !hideUntrustedInteractions || isUserTrusted(repost.pubkey))
|
.filter((repost) => !hideUntrustedInteractions || isUserTrusted(repost.pubkey))
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export const ExtendedKind = {
|
|||||||
export const SUPPORTED_KINDS = [
|
export const SUPPORTED_KINDS = [
|
||||||
kinds.ShortTextNote,
|
kinds.ShortTextNote,
|
||||||
kinds.Repost,
|
kinds.Repost,
|
||||||
|
kinds.GenericRepost,
|
||||||
ExtendedKind.PICTURE,
|
ExtendedKind.PICTURE,
|
||||||
ExtendedKind.VIDEO,
|
ExtendedKind.VIDEO,
|
||||||
ExtendedKind.SHORT_VIDEO,
|
ExtendedKind.SHORT_VIDEO,
|
||||||
|
|||||||
@@ -118,12 +118,22 @@ export function createRepostDraftEvent(event: Event): TDraftEvent {
|
|||||||
const isProtected = isProtectedEvent(event)
|
const isProtected = isProtectedEvent(event)
|
||||||
const tags = [buildETag(event.id, event.pubkey), buildPTag(event.pubkey)]
|
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)) {
|
if (isReplaceableEvent(event.kind)) {
|
||||||
tags.push(buildATag(event))
|
tags.push(buildATag(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: kinds.Repost,
|
kind: kinds.GenericRepost,
|
||||||
content: isProtected ? '' : JSON.stringify(event),
|
content: isProtected ? '' : JSON.stringify(event),
|
||||||
tags,
|
tags,
|
||||||
created_at: dayjs().unix()
|
created_at: dayjs().unix()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
TThemeSetting,
|
TThemeSetting,
|
||||||
TTranslationServiceConfig
|
TTranslationServiceConfig
|
||||||
} from '@/types'
|
} from '@/types'
|
||||||
|
import { kinds } from 'nostr-tools'
|
||||||
|
|
||||||
class LocalStorageService {
|
class LocalStorageService {
|
||||||
static instance: LocalStorageService
|
static instance: LocalStorageService
|
||||||
@@ -181,10 +182,13 @@ class LocalStorageService {
|
|||||||
showKindSet.delete(24236) // remove typo kind
|
showKindSet.delete(24236) // remove typo kind
|
||||||
showKindSet.add(ExtendedKind.ADDRESSABLE_SHORT_VIDEO)
|
showKindSet.add(ExtendedKind.ADDRESSABLE_SHORT_VIDEO)
|
||||||
}
|
}
|
||||||
|
if (showKindsVersion < 4 && showKindSet.has(kinds.Repost)) {
|
||||||
|
showKindSet.add(kinds.GenericRepost)
|
||||||
|
}
|
||||||
this.showKinds = Array.from(showKindSet)
|
this.showKinds = Array.from(showKindSet)
|
||||||
}
|
}
|
||||||
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(this.showKinds))
|
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 =
|
this.hideContentMentioningMutedUsers =
|
||||||
window.localStorage.getItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'
|
window.localStorage.getItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'
|
||||||
|
|||||||
@@ -107,7 +107,10 @@ class StuffStatsService {
|
|||||||
? {
|
? {
|
||||||
'#e': [event.id],
|
'#e': [event.id],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [kinds.Reaction, kinds.Repost]
|
kinds:
|
||||||
|
event.kind === kinds.ShortTextNote
|
||||||
|
? [kinds.Reaction, kinds.Repost]
|
||||||
|
: [kinds.Reaction, kinds.Repost, kinds.GenericRepost]
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
'#i': [externalContent],
|
'#i': [externalContent],
|
||||||
@@ -120,7 +123,7 @@ class StuffStatsService {
|
|||||||
filters.push({
|
filters.push({
|
||||||
'#a': [replaceableCoordinate],
|
'#a': [replaceableCoordinate],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [kinds.Reaction, kinds.Repost]
|
kinds: [kinds.Reaction, kinds.Repost, kinds.GenericRepost]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +221,7 @@ class StuffStatsService {
|
|||||||
targetKey = this.addLikeByEvent(evt)
|
targetKey = this.addLikeByEvent(evt)
|
||||||
} else if (evt.kind === ExtendedKind.EXTERNAL_CONTENT_REACTION) {
|
} else if (evt.kind === ExtendedKind.EXTERNAL_CONTENT_REACTION) {
|
||||||
targetKey = this.addExternalContentLikeByEvent(evt)
|
targetKey = this.addExternalContentLikeByEvent(evt)
|
||||||
} else if (evt.kind === kinds.Repost) {
|
} else if (evt.kind === kinds.Repost || evt.kind === kinds.GenericRepost) {
|
||||||
targetKey = this.addRepostByEvent(evt)
|
targetKey = this.addRepostByEvent(evt)
|
||||||
} else if (evt.kind === kinds.Zap) {
|
} else if (evt.kind === kinds.Zap) {
|
||||||
targetKey = this.addZapByEvent(evt)
|
targetKey = this.addZapByEvent(evt)
|
||||||
@@ -291,18 +294,25 @@ class StuffStatsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addRepostByEvent(evt: Event) {
|
private addRepostByEvent(evt: Event) {
|
||||||
const eventId = evt.tags.find(tagNameEquals('e'))?.[1]
|
let targetEventKey
|
||||||
if (!eventId) return
|
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 repostPubkeySet = old.repostPubkeySet || new Set()
|
||||||
const reposts = old.reposts || []
|
const reposts = old.reposts || []
|
||||||
if (repostPubkeySet.has(evt.pubkey)) return
|
if (repostPubkeySet.has(evt.pubkey)) return
|
||||||
|
|
||||||
repostPubkeySet.add(evt.pubkey)
|
repostPubkeySet.add(evt.pubkey)
|
||||||
reposts.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
|
reposts.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
|
||||||
this.stuffStatsMap.set(eventId, { ...old, repostPubkeySet, reposts })
|
this.stuffStatsMap.set(targetEventKey, { ...old, repostPubkeySet, reposts })
|
||||||
return eventId
|
return targetEventKey
|
||||||
}
|
}
|
||||||
|
|
||||||
private addZapByEvent(evt: Event) {
|
private addZapByEvent(evt: Event) {
|
||||||
|
|||||||
Reference in New Issue
Block a user