Browse Source

inlcude kind 16 reposts

imwald
Silberengel 1 month ago
parent
commit
f3e2c3f58d
  1. 4
      src/components/ContentPreview/index.tsx
  2. 2
      src/components/KindFilter/index.tsx
  3. 3
      src/components/LatestFromFollowsSection/index.tsx
  4. 5
      src/components/Note/NotificationEventCard.tsx
  5. 10
      src/components/Note/index.tsx
  6. 2
      src/components/NoteBoostBadges/index.tsx
  7. 39
      src/components/NoteCard/RepostNoteCard.tsx
  8. 6
      src/components/NoteCard/index.tsx
  9. 22
      src/components/NoteStats/SeenOnButton.tsx
  10. 3
      src/components/Profile/ProfileFeed.tsx
  11. 9
      src/components/Profile/ProfileFeedWithPins.tsx
  12. 9
      src/constants.ts
  13. 9
      src/lib/draft-event.ts
  14. 24
      src/lib/event.ts
  15. 2
      src/lib/kind-description.ts
  16. 6
      src/pages/primary/SpellsPage/index.tsx
  17. 1
      src/pages/secondary/NotePage/index.tsx
  18. 2
      src/providers/NostrProvider/index.tsx
  19. 2
      src/services/client-events.service.ts
  20. 12
      src/services/client-query.service.ts
  21. 25
      src/services/client.service.ts
  22. 1
      src/services/nip89.service.ts
  23. 35
      src/services/note-stats.service.ts

4
src/components/ContentPreview/index.tsx

@ -4,7 +4,7 @@ import { @@ -4,7 +4,7 @@ import {
notificationReactionSummaryKey,
useNotificationReactionDisplay
} from '@/hooks/useNotificationReactionDisplay'
import { isMentioningMutedUsers, isNip25ReactionKind } from '@/lib/event'
import { isMentioningMutedUsers, isNip18RepostKind, isNip25ReactionKind } from '@/lib/event'
import {
DISCUSSION_DOWNVOTE_DISPLAY,
DISCUSSION_UPVOTE_DISPLAY
@ -204,7 +204,7 @@ export default function ContentPreview({ @@ -204,7 +204,7 @@ export default function ContentPreview({
)
}
if (event.kind === kinds.Repost) {
if (isNip18RepostKind(event.kind)) {
return withKindRow(
<div className="pointer-events-none text-sm text-muted-foreground">{t('Notification boost summary')}</div>
)

2
src/components/KindFilter/index.tsx

@ -27,7 +27,7 @@ const KIND_FILTER_OPTIONS = [ @@ -27,7 +27,7 @@ const KIND_FILTER_OPTIONS = [
{ kindGroup: [ExtendedKind.DISCUSSION], label: 'Discussions' },
{ kindGroup: [ExtendedKind.CALENDAR_EVENT_DATE, ExtendedKind.CALENDAR_EVENT_TIME], label: 'Calendar Events' },
{ kindGroup: [ExtendedKind.ZAP_RECEIPT], label: 'Zaps' },
{ kindGroup: [kinds.Repost], label: 'Boosts' }
{ kindGroup: [kinds.Repost, ExtendedKind.GENERIC_REPOST], label: 'Boosts' }
]
function buildShowKindsFromOptions(

3
src/components/LatestFromFollowsSection/index.tsx

@ -54,7 +54,8 @@ const FEED_KINDS = [ @@ -54,7 +54,8 @@ const FEED_KINDS = [
ExtendedKind.VIDEO,
ExtendedKind.SHORT_VIDEO,
ExtendedKind.COMMENT,
kinds.Repost
kinds.Repost,
ExtendedKind.GENERIC_REPOST
] as number[]
const feedKindSet = new Set(FEED_KINDS)

5
src/components/Note/NotificationEventCard.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { ExtendedKind } from '@/constants'
import { isNip18RepostKind } from '@/lib/event'
import { cn } from '@/lib/utils'
import { Event, kinds } from 'nostr-tools'
import { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next'
/**
@ -10,7 +11,7 @@ import { useTranslation } from 'react-i18next' @@ -10,7 +11,7 @@ import { useTranslation } from 'react-i18next'
export default function NotificationEventCard({ event, className }: { event: Event; className?: string }) {
const { t } = useTranslation()
if (event.kind === kinds.Repost) {
if (isNip18RepostKind(event.kind)) {
return (
<div
className={cn(

10
src/components/Note/index.tsx

@ -1,7 +1,13 @@ @@ -1,7 +1,13 @@
import { useSmartNoteNavigationOptional } from '@/PageManager'
import { ExtendedKind } from '@/constants'
import { isRenderableNoteKind } from '@/lib/note-renderable-kinds'
import { getHttpUrlFromITags, getParentBech32Id, isNip25ReactionKind, isNsfwEvent } from '@/lib/event'
import {
getHttpUrlFromITags,
getParentBech32Id,
isNip18RepostKind,
isNip25ReactionKind,
isNsfwEvent
} from '@/lib/event'
import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils'
import {
@ -157,7 +163,7 @@ export default function Note({ @@ -157,7 +163,7 @@ export default function Note({
content = <NsfwNote show={() => setShowNsfw(true)} />
} else if (isNip25ReactionKind(event.kind)) {
content = null
} else if (event.kind === kinds.Repost || event.kind === ExtendedKind.POLL_RESPONSE) {
} else if (isNip18RepostKind(event.kind) || event.kind === ExtendedKind.POLL_RESPONSE) {
content = <NotificationEventCard className="mt-2" event={event} />
} else if (event.kind === kinds.Highlights) {
// Try to render the Highlight component with error boundary

2
src/components/NoteBoostBadges/index.tsx

@ -11,7 +11,7 @@ import UserAvatar from '../UserAvatar' @@ -11,7 +11,7 @@ import UserAvatar from '../UserAvatar'
const MAX_VISIBLE = 28
/**
* Small avatar strip of users who boosted (kind 6) the note shown under the OP on the note page.
* Small avatar strip of users who boosted (kind 6 / 16) the note shown under the OP on the note page.
*/
export default function NoteBoostBadges({ event, className }: { event: Event; className?: string }) {
const { t } = useTranslation()

39
src/components/NoteCard/RepostNoteCard.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { ExtendedKind } from '@/constants'
import { isMentioningMutedUsers } from '@/lib/event'
import { tagNameEquals } from '@/lib/tag'
import { generateBech32IdFromATag, getFirstHexEventIdFromETags, tagNameEquals } from '@/lib/tag'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/contexts/mute-list-context'
import client from '@/services/client.service'
@ -37,7 +38,7 @@ export default function RepostNoteCard({ @@ -37,7 +38,7 @@ export default function RepostNoteCard({
try {
const eventFromContent = event.content ? (JSON.parse(event.content) as Event) : null
if (eventFromContent && verifyEvent(eventFromContent)) {
if (eventFromContent.kind === kinds.Repost) {
if (eventFromContent.kind === kinds.Repost || eventFromContent.kind === ExtendedKind.GENERIC_REPOST) {
return
}
client.addEventToCache(eventFromContent)
@ -52,18 +53,32 @@ export default function RepostNoteCard({ @@ -52,18 +53,32 @@ export default function RepostNoteCard({
return
}
const [, id, relay, , pubkey] = event.tags.find(tagNameEquals('e')) ?? []
if (!id) {
const hex = getFirstHexEventIdFromETags(event.tags)
if (hex) {
const row =
event.tags.find((t) => (t[0] === 'e' || t[0] === 'E') && t[1] === hex) ?? []
const [, id, relay, , pubkey] = row
const targetEventId = nip19.neventEncode({
id,
relays: relay ? [relay] : [],
author: pubkey
})
const targetEvent = await eventService.fetchEvent(targetEventId)
if (targetEvent) {
setTargetEvent(targetEvent)
}
return
}
const targetEventId = nip19.neventEncode({
id,
relays: relay ? [relay] : [],
author: pubkey
})
const targetEvent = await eventService.fetchEvent(targetEventId)
if (targetEvent) {
setTargetEvent(targetEvent)
if (event.kind === ExtendedKind.GENERIC_REPOST) {
const aRow = event.tags.find(tagNameEquals('a')) ?? event.tags.find(tagNameEquals('A'))
const naddr = aRow ? generateBech32IdFromATag(aRow) : undefined
if (naddr) {
const ev = await eventService.fetchEvent(naddr)
if (ev) {
setTargetEvent(ev)
}
}
}
} catch {
// ignore

6
src/components/NoteCard/index.tsx

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
import { Skeleton } from '@/components/ui/skeleton'
import { isMentioningMutedUsers } from '@/lib/event'
import { isMentioningMutedUsers, isNip18RepostKind } from '@/lib/event'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/contexts/mute-list-context'
import { Event, kinds } from 'nostr-tools'
import { Event } from 'nostr-tools'
import { memo, useMemo } from 'react'
import MainNoteCard from './MainNoteCard'
import RepostNoteCard from './RepostNoteCard'
@ -36,7 +36,7 @@ const NoteCard = memo(function NoteCard({ @@ -36,7 +36,7 @@ const NoteCard = memo(function NoteCard({
}, [event, filterMutedNotes, mutePubkeySet])
if (shouldHide) return null
if (event.kind === kinds.Repost) {
if (isNip18RepostKind(event.kind)) {
return (
<RepostNoteCard
event={event}

22
src/components/NoteStats/SeenOnButton.tsx

@ -27,9 +27,25 @@ export default function SeenOnButton({ event }: { event: Event }) { @@ -27,9 +27,25 @@ export default function SeenOnButton({ event }: { event: Event }) {
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
useEffect(() => {
const seenOn = client.getSeenEventRelayUrls(event.id)
setRelays(seenOn)
}, [])
let cancelled = false
let attempts = 0
const maxAttempts = 20
const apply = () => {
const seenOn = client.getSeenEventRelayUrls(event.id)
if (!cancelled) setRelays(seenOn)
return seenOn.length > 0
}
if (apply()) return
const id = setInterval(() => {
if (cancelled) return
attempts++
if (apply() || attempts >= maxAttempts) clearInterval(id)
}, 500)
return () => {
cancelled = true
clearInterval(id)
}
}, [event.id])
const trigger = (
<button

3
src/components/Profile/ProfileFeed.tsx

@ -8,6 +8,7 @@ import ProfileTimeline from './ProfileTimeline' @@ -8,6 +8,7 @@ import ProfileTimeline from './ProfileTimeline'
const POST_KIND_LIST = [
kinds.ShortTextNote,
kinds.Repost,
ExtendedKind.GENERIC_REPOST,
ExtendedKind.COMMENT,
ExtendedKind.DISCUSSION,
ExtendedKind.POLL,
@ -49,7 +50,7 @@ const ProfileFeed = forwardRef<{ refresh: () => void; getEvents?: () => Event[] @@ -49,7 +50,7 @@ const ProfileFeed = forwardRef<{ refresh: () => void; getEvents?: () => Event[]
if (!kindValue || kindValue === 'all') return 'posts'
const kindNum = parseInt(kindValue, 10)
if (kindNum === kinds.ShortTextNote) return 'notes'
if (kindNum === kinds.Repost) return 'boosts'
if (kindNum === kinds.Repost || kindNum === ExtendedKind.GENERIC_REPOST) return 'boosts'
if (kindNum === ExtendedKind.COMMENT) return 'comments'
if (kindNum === ExtendedKind.DISCUSSION) return 'discussions'
if (kindNum === ExtendedKind.POLL) return 'polls'

9
src/components/Profile/ProfileFeedWithPins.tsx

@ -45,8 +45,13 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string @@ -45,8 +45,13 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string
const { showKinds, showKind1OPs, showKind1Replies, showKind1111 } = useKindFilter()
/** Profile timelines always show reposts; global kind filter still applies to other kinds. */
const profileTimelineShowKinds = useMemo(() => {
if (showKinds.includes(kinds.Repost)) return showKinds
return [...showKinds, kinds.Repost].sort((a, b) => a - b)
if (showKinds.includes(kinds.Repost) && showKinds.includes(ExtendedKind.GENERIC_REPOST)) {
return showKinds
}
const next = [...showKinds]
if (!next.includes(kinds.Repost)) next.push(kinds.Repost)
if (!next.includes(ExtendedKind.GENERIC_REPOST)) next.push(ExtendedKind.GENERIC_REPOST)
return next.sort((a, b) => a - b)
}, [showKinds])
const hideReplies = useHideRepliesLikeMainFeed()
const [searchQuery, setSearchQuery] = useState('')

9
src/constants.ts

@ -318,6 +318,8 @@ export const ExtendedKind = { @@ -318,6 +318,8 @@ export const ExtendedKind = {
RSS_FEED_LIST: 10895,
/** Client-only synthetic "parent" for RSS article threads; never published to relays */
RSS_THREAD_ROOT: 99999,
/** NIP-18: generic repost of any event other than kind 1 (kind 6 is kind-1-only). */
GENERIC_REPOST: 16,
/** NIP-25: reaction to external content (NIP-73 `k` + `i`), e.g. http(s) URLs */
EXTERNAL_REACTION: 17,
// NIP-89 Application Handlers
@ -397,6 +399,7 @@ export const MAX_CALENDAR_INVITEES = 10 @@ -397,6 +399,7 @@ export const MAX_CALENDAR_INVITEES = 10
export const SUPPORTED_KINDS = [
kinds.ShortTextNote,
kinds.Repost,
ExtendedKind.GENERIC_REPOST,
ExtendedKind.PICTURE,
ExtendedKind.VIDEO,
ExtendedKind.SHORT_VIDEO,
@ -435,10 +438,12 @@ export const PROFILE_FEED_KINDS = SUPPORTED_KINDS.filter( @@ -435,10 +438,12 @@ export const PROFILE_FEED_KINDS = SUPPORTED_KINDS.filter(
)
/**
* {@link PROFILE_FEED_KINDS} without reposts (kind 6). Default for the global kind filter, home feed,
* {@link PROFILE_FEED_KINDS} without reposts (kind 6 / 16). Default for the global kind filter, home feed,
* and most faux spells. Reposts are still shown on profile timelines, Spells Following, and Follows latest.
*/
export const DEFAULT_FEED_SHOW_KINDS = PROFILE_FEED_KINDS.filter((k) => k !== kinds.Repost)
export const DEFAULT_FEED_SHOW_KINDS = PROFILE_FEED_KINDS.filter(
(k) => k !== kinds.Repost && k !== ExtendedKind.GENERIC_REPOST
)
/** Order for faux-spells in the feed / spell picker. */
export const FAUX_SPELL_ORDER = [

9
src/lib/draft-event.ts

@ -125,14 +125,19 @@ export function createReactionDraftEvent(event: Event, emoji: TEmoji | string = @@ -125,14 +125,19 @@ export function createReactionDraftEvent(event: Event, emoji: TEmoji | string =
// https://github.com/nostr-protocol/nips/blob/master/18.md
export function createRepostDraftEvent(event: Event): TDraftEvent {
const isProtected = isProtectedEvent(event)
const tags = [buildETag(event.id, event.pubkey), buildPTag(event.pubkey)]
const tags: string[][] = [buildETag(event.id, event.pubkey), buildPTag(event.pubkey)]
if (isReplaceableEvent(event.kind)) {
tags.push(buildATag(event))
}
const useGenericRepost = event.kind !== kinds.ShortTextNote
if (useGenericRepost) {
tags.push(['k', String(event.kind)])
}
return {
kind: kinds.Repost,
kind: useGenericRepost ? ExtendedKind.GENERIC_REPOST : kinds.Repost,
content: isProtected ? '' : JSON.stringify(event),
tags,
created_at: dayjs().unix()

24
src/lib/event.ts

@ -20,6 +20,11 @@ export function isNip25ReactionKind(kind: number): boolean { @@ -20,6 +20,11 @@ export function isNip25ReactionKind(kind: number): boolean {
return kind === kinds.Reaction || kind === ExtendedKind.EXTERNAL_REACTION
}
/** NIP-18: kind 6 (kind-1 repost) or kind 16 (generic repost). */
export function isNip18RepostKind(kind: number): boolean {
return kind === kinds.Repost || kind === ExtendedKind.GENERIC_REPOST
}
const EVENT_EMBEDDED_NOTES_CACHE = new LRUCache<string, string[]>({ max: 10000 })
const EVENT_EMBEDDED_PUBKEYS_CACHE = new LRUCache<string, string[]>({ max: 10000 })
const EVENT_IS_REPLY_NOTE_CACHE = new LRUCache<string, boolean>({ max: 10000 })
@ -83,12 +88,8 @@ export function isMentioningMutedUsers(event: Event, mutePubkeySet: Set<string>) @@ -83,12 +88,8 @@ export function isMentioningMutedUsers(event: Event, mutePubkeySet: Set<string>)
export function getParentETag(event?: Event) {
if (!event) return undefined
// NIP-25 reactions, NIP-18 reposts, poll responses: first hex `e` / `E` references the target note.
if (
event.kind === kinds.Reaction ||
event.kind === kinds.Repost ||
event.kind === ExtendedKind.POLL_RESPONSE
) {
// NIP-25 reactions, NIP-18 reposts (6 / 16), poll responses: first hex `e` / `E` references the target note.
if (event.kind === kinds.Reaction || isNip18RepostKind(event.kind) || event.kind === ExtendedKind.POLL_RESPONSE) {
const firstId = getFirstHexEventIdFromETags(event.tags)
if (!firstId) return undefined
return (
@ -120,14 +121,15 @@ export function getParentETag(event?: Event) { @@ -120,14 +121,15 @@ export function getParentETag(event?: Event) {
if (event.kind !== kinds.ShortTextNote) return undefined
const isETag = (n: string) => n === 'e' || n === 'E'
let tag = event.tags.find(([tagName, , , marker]) => {
return tagName === 'e' && marker === 'reply'
return isETag(tagName) && marker === 'reply'
})
if (!tag) {
const embeddedEventIds = getEmbeddedNoteBech32Ids(event)
tag = event.tags.findLast(
([tagName, tagValue, , marker]) =>
tagName === 'e' &&
isETag(tagName) &&
!!tagValue &&
marker !== 'mention' &&
!embeddedEventIds.includes(tagValue)
@ -191,13 +193,15 @@ export function getRootETag(event?: Event) { @@ -191,13 +193,15 @@ export function getRootETag(event?: Event) {
if (event.kind !== kinds.ShortTextNote) return undefined
const isETag = (n: string) => n === 'e' || n === 'E'
let tag = event.tags.find(([tagName, , , marker]) => {
return tagName === 'e' && marker === 'root'
return isETag(tagName) && marker === 'root'
})
if (!tag) {
const embeddedEventIds = getEmbeddedNoteBech32Ids(event)
tag = event.tags.find(
([tagName, tagValue]) => tagName === 'e' && !!tagValue && !embeddedEventIds.includes(tagValue)
([tagName, tagValue]) =>
isETag(tagName) && !!tagValue && !embeddedEventIds.includes(tagValue)
)
}
return tag

2
src/lib/kind-description.ts

@ -66,6 +66,8 @@ export function getKindDescription( @@ -66,6 +66,8 @@ export function getKindDescription(
return { number: 0, description: 'Profile metadata' }
case kinds.Repost:
return { number: 6, description: 'Repost' }
case ExtendedKind.GENERIC_REPOST:
return { number: 16, description: 'Generic repost' }
case kinds.Reaction:
return { number: 7, description: 'Reaction' }
case ExtendedKind.EXTERNAL_REACTION:

6
src/pages/primary/SpellsPage/index.tsx

@ -982,8 +982,10 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -982,8 +982,10 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
if (selectedFauxSpell && isFollowFeedFauxSpellId(selectedFauxSpell)) {
// Profile feed kinds omit boosts; show reposts as cards in this faux spell only.
const k = kindFilterShowKinds
if (k.includes(nostrKinds.Repost)) return k
return [...k, nostrKinds.Repost].sort((a, b) => a - b)
const out = [...k]
if (!out.includes(nostrKinds.Repost)) out.push(nostrKinds.Repost)
if (!out.includes(ExtendedKind.GENERIC_REPOST)) out.push(ExtendedKind.GENERIC_REPOST)
return out.sort((a, b) => a - b)
}
if (selectedFauxSpell === 'followPacks') {
return [ExtendedKind.FOLLOW_PACK]

1
src/pages/secondary/NotePage/index.tsx

@ -187,6 +187,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }: @@ -187,6 +187,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
case 9735: // ExtendedKind.ZAP_RECEIPT
return 'Note: Zap Receipt'
case 6: // kinds.Repost (Nostr boost)
case 16: // ExtendedKind.GENERIC_REPOST (NIP-18)
return 'Note: Boost'
case 7: // kinds.Reaction
return 'Note: Reaction'

2
src/providers/NostrProvider/index.tsx

@ -734,7 +734,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -734,7 +734,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const events = await queryService.fetchEvents(relayList.write.slice(0, 4), [
{
authors: [pubkey],
kinds: [kinds.Reaction, ExtendedKind.EXTERNAL_REACTION, kinds.Repost],
kinds: [kinds.Reaction, ExtendedKind.EXTERNAL_REACTION, kinds.Repost, ExtendedKind.GENERIC_REPOST],
limit: 100
},
{

2
src/services/client-events.service.ts

@ -471,7 +471,7 @@ export class EventService { @@ -471,7 +471,7 @@ export class EventService {
const qref = getQuotedReferenceFromQTags(ev)
add(qref?.hexId)
add(qref?.coordinate)
if (ev.kind === kinds.Zap) {
if (ev.kind === kinds.Zap || ev.kind === kinds.Repost || ev.kind === ExtendedKind.GENERIC_REPOST) {
add(getFirstHexEventIdFromETags(ev.tags))
}
if (

12
src/services/client-query.service.ts

@ -154,18 +154,24 @@ export class QueryService { @@ -154,18 +154,24 @@ export class QueryService {
}
}
private canonicalSeenOnEventId(eventId: string): string {
const t = eventId.trim()
return /^[0-9a-f]{64}$/i.test(t) ? t.toLowerCase() : t
}
trackEventSeenOn(eventId: string, relay: AbstractRelay): void {
const id = this.canonicalSeenOnEventId(eventId)
const url = relay.url
let set = this.eventSeenOnRelays.get(eventId)
let set = this.eventSeenOnRelays.get(id)
if (!set) {
set = new Set()
this.eventSeenOnRelays.set(eventId, set)
this.eventSeenOnRelays.set(id, set)
}
set.add(url)
}
getSeenEventRelayUrls(eventId: string): string[] {
return Array.from(this.eventSeenOnRelays.get(eventId) ?? [])
return Array.from(this.eventSeenOnRelays.get(this.canonicalSeenOnEventId(eventId)) ?? [])
}
/**

25
src/services/client.service.ts

@ -20,6 +20,12 @@ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter { @@ -20,6 +20,12 @@ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter {
const { search: _search, ...rest } = f
return rest as Filter
}
/** Single key for `pool.seenOn` / query seen-on maps (hex ids are case-insensitive). */
function canonicalSeenOnEventId(eventId: string): string {
const t = eventId.trim()
return /^[0-9a-f]{64}$/i.test(t) ? t.toLowerCase() : t
}
import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
@ -2116,11 +2122,19 @@ class ClientService extends EventTarget { @@ -2116,11 +2122,19 @@ class ClientService extends EventTarget {
/** =========== Event =========== */
getSeenEventRelays(eventId: string) {
return Array.from(this.pool.seenOn.get(eventId)?.values() || [])
const key = canonicalSeenOnEventId(eventId)
return Array.from(this.pool.seenOn.get(key)?.values() || [])
}
getSeenEventRelayUrls(eventId: string) {
return this.getSeenEventRelays(eventId).map((relay) => relay.url)
/**
* Relays that delivered this event: {@link SimplePool.seenOn} (live subs) plus
* {@link QueryService}s map for `query()` / `fetchEvents` REQs.
*/
getSeenEventRelayUrls(eventId: string): string[] {
const key = canonicalSeenOnEventId(eventId)
const poolUrls = this.getSeenEventRelays(key).map((r) => normalizeUrl(r.url) || r.url)
const queryUrls = this.queryService.getSeenEventRelayUrls(key).map((u) => normalizeUrl(u) || u)
return Array.from(new Set([...poolUrls, ...queryUrls].filter(Boolean)))
}
getEventHints(eventId: string) {
@ -2132,10 +2146,11 @@ class ClientService extends EventTarget { @@ -2132,10 +2146,11 @@ class ClientService extends EventTarget {
}
trackEventSeenOn(eventId: string, relay: AbstractRelay) {
let set = this.pool.seenOn.get(eventId)
const key = canonicalSeenOnEventId(eventId)
let set = this.pool.seenOn.get(key)
if (!set) {
set = new Set()
this.pool.seenOn.set(eventId, set)
this.pool.seenOn.set(key, set)
}
set.add(relay)
}

1
src/services/nip89.service.ts

@ -211,6 +211,7 @@ class Nip89Service { @@ -211,6 +211,7 @@ class Nip89Service {
supportedKinds: [
kinds.ShortTextNote,
kinds.Repost,
ExtendedKind.GENERIC_REPOST,
kinds.Reaction,
kinds.Zap,
kinds.LongFormArticle,

35
src/services/note-stats.service.ts

@ -5,7 +5,7 @@ import { @@ -5,7 +5,7 @@ import {
SEARCHABLE_RELAY_URLS
} from '@/constants'
import { replaceStandardEmojiShortcodesInContent } from '@/lib/emoji-content'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { getReplaceableCoordinateFromEvent, isNip18RepostKind, isReplaceableEvent } from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import {
@ -288,6 +288,7 @@ class NoteStatsService { @@ -288,6 +288,7 @@ class NoteStatsService {
): { nonSocial: Filter[]; social: Filter[] } {
const reactionLimit = 300
const interactionLimit = 80
const nip18RepostKinds = [kinds.Repost, ExtendedKind.GENERIC_REPOST]
/** Synthetic RSS/Web parents are not on relays; `#e` on the fake id returns nothing. Use only URL-scoped filters. */
if (event.kind === ExtendedKind.RSS_THREAD_ROOT) {
@ -326,7 +327,7 @@ class NoteStatsService { @@ -326,7 +327,7 @@ class NoteStatsService {
{
'#e': [event.id],
kinds: [
kinds.Repost,
...nip18RepostKinds,
kinds.ShortTextNote,
ExtendedKind.COMMENT,
ExtendedKind.VOICE_COMMENT,
@ -350,7 +351,7 @@ class NoteStatsService { @@ -350,7 +351,7 @@ class NoteStatsService {
{
'#a': [replaceableCoordinate],
kinds: [
kinds.Repost,
...nip18RepostKinds,
kinds.ShortTextNote,
ExtendedKind.COMMENT,
ExtendedKind.VOICE_COMMENT,
@ -463,7 +464,7 @@ class NoteStatsService { @@ -463,7 +464,7 @@ class NoteStatsService {
originalEventAuthor,
mergeOpts?.interactionTargetNoteId
)
} else if (evt.kind === kinds.Repost) {
} else if (isNip18RepostKind(evt.kind)) {
updatedEventId = this.addRepostByEvent(evt, originalEventAuthor, mergeOpts?.interactionTargetNoteId)
} else if (evt.kind === kinds.Zap) {
updatedEventId = this.addZapByEvent(evt, originalEventAuthor)
@ -587,8 +588,32 @@ class NoteStatsService { @@ -587,8 +588,32 @@ class NoteStatsService {
return eventId
}
private repostStatsTargetId(evt: Event, forcedTargetEventId?: string): string | undefined {
const forced = forcedTargetEventId?.trim()
if (forced) return forced
const hex = getFirstHexEventIdFromETags(evt.tags)
if (hex) return hex.toLowerCase()
if (evt.kind === ExtendedKind.GENERIC_REPOST) {
const aTag = evt.tags.find(tagNameEquals('a')) ?? evt.tags.find(tagNameEquals('A'))
const coord = aTag?.[1]?.trim()
if (coord) return coord
const raw = evt.content?.trim()
if (raw) {
try {
const embedded = JSON.parse(raw) as { id?: string }
if (embedded.id && /^[0-9a-f]{64}$/i.test(embedded.id)) {
return embedded.id.toLowerCase()
}
} catch {
/* ignore */
}
}
}
return undefined
}
private addRepostByEvent(evt: Event, originalEventAuthor?: string, forcedTargetEventId?: string) {
const eventId = forcedTargetEventId ?? getFirstHexEventIdFromETags(evt.tags)
const eventId = this.repostStatsTargetId(evt, forcedTargetEventId)
if (!eventId) return
const old = this.noteStatsMap.get(eventId) || {}

Loading…
Cancel
Save