Browse Source

Personalized trending algos

Follow hashtags and pin notes
imwald
Silberengel 5 months ago
parent
commit
e8d62d6e31
  1. 5
      src/components/NoteList/index.tsx
  2. 87
      src/components/NoteOptions/useMenuActions.tsx
  3. 63
      src/components/Profile/ProfileFeed.tsx
  4. 120
      src/components/TrendingNotes/index.tsx
  5. 56
      src/pages/secondary/NoteListPage/index.tsx
  6. 8
      src/providers/NostrProvider/index.tsx
  7. 4
      src/services/client.service.ts
  8. 8
      src/services/indexed-db.service.ts
  9. 71
      src/services/note-stats.service.ts

5
src/components/NoteList/index.tsx

@ -45,7 +45,8 @@ const NoteList = forwardRef( @@ -45,7 +45,8 @@ const NoteList = forwardRef(
hideReplies = false,
hideUntrustedNotes = false,
areAlgoRelays = false,
showRelayCloseReason = false
showRelayCloseReason = false,
customHeader
}: {
subRequests: TFeedSubRequest[]
showKinds: number[]
@ -54,6 +55,7 @@ const NoteList = forwardRef( @@ -54,6 +55,7 @@ const NoteList = forwardRef(
hideUntrustedNotes?: boolean
areAlgoRelays?: boolean
showRelayCloseReason?: boolean
customHeader?: React.ReactNode
},
ref
) => {
@ -299,6 +301,7 @@ const NoteList = forwardRef( @@ -299,6 +301,7 @@ const NoteList = forwardRef(
const list = (
<div className="min-h-screen">
{customHeader}
{filteredEvents.map((event) => (
<NoteCard
key={event.id}

87
src/components/NoteOptions/useMenuActions.tsx

@ -8,7 +8,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -8,7 +8,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert } from 'lucide-react'
import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert, Pin } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
@ -49,7 +49,7 @@ export function useMenuActions({ @@ -49,7 +49,7 @@ export function useMenuActions({
isSmallScreen
}: UseMenuActionsProps) {
const { t } = useTranslation()
const { pubkey, attemptDelete } = useNostr()
const { pubkey, attemptDelete, publish } = useNostr()
const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays()
const { relaySets, favoriteRelays } = useFavoriteRelays()
const relayUrls = useMemo(() => {
@ -61,6 +61,72 @@ export function useMenuActions({ @@ -61,6 +61,72 @@ export function useMenuActions({
const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList()
const isMuted = useMemo(() => mutePubkeySet.has(event.pubkey), [mutePubkeySet, event])
// Check if event is pinned
const [isPinned, setIsPinned] = useState(false)
useEffect(() => {
const checkIfPinned = async () => {
if (!pubkey) {
setIsPinned(false)
return
}
try {
const pinListEvent = await client.fetchPinListEvent(pubkey)
if (pinListEvent) {
const isEventPinned = pinListEvent.tags.some(tag => tag[0] === 'e' && tag[1] === event.id)
setIsPinned(isEventPinned)
}
} catch (error) {
console.error('Error checking pin status:', error)
}
}
checkIfPinned()
}, [pubkey, event.id])
const handlePinNote = async () => {
if (!pubkey) return
try {
// Fetch existing pin list
let pinListEvent = await client.fetchPinListEvent(pubkey)
// Get existing event IDs, excluding the one we're toggling
const existingEventIds = (pinListEvent?.tags || [])
.filter(tag => tag[0] === 'e' && tag[1])
.map(tag => tag[1])
.filter(id => id !== event.id)
let newTags: string[][]
let successMessage: string
if (isPinned) {
// Unpin: just keep the existing tags without this event
newTags = existingEventIds.map(id => ['e', id])
successMessage = t('Note unpinned')
} else {
// Pin: add this event to the existing list
newTags = [...existingEventIds.map(id => ['e', id]), ['e', event.id]]
successMessage = t('Note pinned')
}
// Create and publish the new pin list event
await publish({
kind: 10001,
tags: newTags,
content: '',
created_at: Math.floor(Date.now() / 1000)
})
// Update local state - the publish will update the cache automatically
setIsPinned(!isPinned)
toast.success(successMessage)
closeDrawer()
} catch (error) {
console.error('Error pinning/unpinning note:', error)
toast.error(t('Failed to pin note'))
}
}
// Check if this is a reply to a discussion event
const [isReplyToDiscussion, setIsReplyToDiscussion] = useState(false)
@ -272,6 +338,14 @@ export function useMenuActions({ @@ -272,6 +338,14 @@ export function useMenuActions({
}
if (pubkey && event.pubkey === pubkey) {
actions.push({
icon: Pin,
label: isPinned ? t('Unpin note') : t('Pin note'),
onClick: () => {
handlePinNote()
},
separator: true
})
actions.push({
icon: Trash2,
label: t('Try deleting this note'),
@ -279,8 +353,7 @@ export function useMenuActions({ @@ -279,8 +353,7 @@ export function useMenuActions({
closeDrawer()
attemptDelete(event)
},
className: 'text-destructive focus:text-destructive',
separator: true
className: 'text-destructive focus:text-destructive'
})
}
@ -295,9 +368,13 @@ export function useMenuActions({ @@ -295,9 +368,13 @@ export function useMenuActions({
closeDrawer,
showSubMenuActions,
setIsRawEventDialogOpen,
setIsReportDialogOpen,
mutePubkeyPrivately,
mutePubkeyPublicly,
unmutePubkey
unmutePubkey,
attemptDelete,
isPinned,
handlePinNote
])
return menuActions

63
src/components/Profile/ProfileFeed.tsx

@ -10,6 +10,8 @@ import storage from '@/services/local-storage.service' @@ -10,6 +10,8 @@ import storage from '@/services/local-storage.service'
import { TFeedSubRequest, TNoteListMode } from '@/types'
import { useEffect, useMemo, useRef, useState } from 'react'
import { RefreshButton } from '../RefreshButton'
import { Event } from 'nostr-tools'
import NoteCard from '../NoteCard'
export default function ProfileFeed({
pubkey,
@ -24,6 +26,9 @@ export default function ProfileFeed({ @@ -24,6 +26,9 @@ export default function ProfileFeed({
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
const noteListRef = useRef<TNoteListRef>(null)
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
const [pinnedEvents, setPinnedEvents] = useState<Event[]>([])
const [loadingPinned, setLoadingPinned] = useState(true)
const tabs = useMemo(() => {
const _tabs = [
{ value: 'posts', label: 'Notes' },
@ -81,6 +86,42 @@ export default function ProfileFeed({ @@ -81,6 +86,42 @@ export default function ProfileFeed({
init()
}, [pubkey, listMode, myPubkey])
// Fetch pinned notes
useEffect(() => {
const fetchPinnedNotes = async () => {
setLoadingPinned(true)
try {
const pinListEvent = await client.fetchPinListEvent(pubkey)
if (pinListEvent && pinListEvent.tags.length > 0) {
// Extract event IDs from pin list
const eventIds = pinListEvent.tags
.filter(tag => tag[0] === 'e' && tag[1])
.map(tag => tag[1])
.reverse() // Reverse to show newest first
// Fetch the actual events
const events = await client.fetchEvents(
[...BIG_RELAY_URLS],
{ ids: eventIds }
)
// Sort by created_at desc (newest first)
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
setPinnedEvents(sortedEvents)
} else {
setPinnedEvents([])
}
} catch (error) {
console.error('Error fetching pinned notes:', error)
setPinnedEvents([])
} finally {
setLoadingPinned(false)
}
}
fetchPinnedNotes()
}, [pubkey])
const handleListModeChange = (mode: TNoteListMode) => {
setListMode(mode)
noteListRef.current?.scrollToTop('smooth')
@ -91,6 +132,27 @@ export default function ProfileFeed({ @@ -91,6 +132,27 @@ export default function ProfileFeed({
noteListRef.current?.scrollToTop()
}
// Create pinned notes header
const pinnedHeader = useMemo(() => {
if (loadingPinned || pinnedEvents.length === 0) return null
return (
<div className="border-b border-border">
<div className="px-4 py-2 bg-muted/30 text-sm font-semibold text-muted-foreground">
Pinned
</div>
{pinnedEvents.map((event) => (
<NoteCard
key={event.id}
className="w-full border-b border-border"
event={event}
filterMutedNotes={false}
/>
))}
</div>
)
}, [pinnedEvents, loadingPinned])
return (
<>
<Tabs
@ -113,6 +175,7 @@ export default function ProfileFeed({ @@ -113,6 +175,7 @@ export default function ProfileFeed({
showKinds={temporaryShowKinds}
hideReplies={listMode === 'posts'}
filterMutedNotes={false}
customHeader={pinnedHeader}
/>
</>
)

120
src/components/TrendingNotes/index.tsx

@ -6,17 +6,137 @@ import client from '@/services/client.service' @@ -6,17 +6,137 @@ import client from '@/services/client.service'
import { NostrEvent } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import noteStatsService from '@/services/note-stats.service'
import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
const SHOW_COUNT = 10
const CACHE_DURATION = 30 * 60 * 1000 // 30 minutes
// Unified cache for all custom trending feeds
let cachedCustomEvents: {
events: Array<{ event: NostrEvent; score: number }>
timestamp: number
hashtags: string[]
listEventIds: string[]
} | null = null
export default function TrendingNotes() {
const { t } = useTranslation()
const { isEventDeleted } = useDeletedEvent()
const { hideUntrustedNotes, isUserTrusted } = useUserTrust()
const { pubkey, relayList } = useNostr()
const { favoriteRelays } = useFavoriteRelays()
const [trendingNotes, setTrendingNotes] = useState<NostrEvent[]>([])
const [showCount, setShowCount] = useState(10)
const [loading, setLoading] = useState(true)
const bottomRef = useRef<HTMLDivElement>(null)
// Get relays based on user login status
const getRelays = useMemo(() => {
const relays: string[] = []
if (pubkey) {
// User is logged in: favorite relays + inboxes (read relays)
relays.push(...favoriteRelays)
if (relayList?.read) {
relays.push(...relayList.read)
}
} else {
// User is not logged in: BIG_RELAY_URLS + FAST_READ_RELAY_URLS
relays.push(...BIG_RELAY_URLS)
relays.push(...FAST_READ_RELAY_URLS)
}
// Normalize and deduplicate
const normalized = relays
.map(url => normalizeUrl(url))
.filter((url): url is string => !!url)
return Array.from(new Set(normalized))
}, [pubkey, favoriteRelays, relayList])
// Initialize or update cache on mount
useEffect(() => {
const initializeCache = async () => {
const now = Date.now()
// Check if cache is still valid
if (cachedCustomEvents && (now - cachedCustomEvents.timestamp) < CACHE_DURATION) {
console.log('[TrendingNotes] Using existing cache')
return
}
console.log('[TrendingNotes] Initializing cache from relays')
const relays = getRelays
try {
// Fetch all events for custom feeds
const twentyFourHoursAgo = Math.floor(Date.now() / 1000) - 24 * 60 * 60
// 1. Fetch top-level posts from last 24 hours
const recentEvents = await client.fetchEvents(relays, {
kinds: [1, 11, 30023, 9802, 20, 21, 22],
since: twentyFourHoursAgo,
limit: 500
})
// Filter for top-level posts only
const topLevelEvents = recentEvents.filter(event => {
const eTags = event.tags.filter(t => t[0] === 'e')
return eTags.length === 0
})
// Fetch stats for events in batches
const eventsNeedingStats = topLevelEvents.filter(event => !noteStatsService.getNoteStats(event.id))
if (eventsNeedingStats.length > 0) {
const batchSize = 10
for (let i = 0; i < eventsNeedingStats.length; i += batchSize) {
const batch = eventsNeedingStats.slice(i, i + batchSize)
await Promise.all(batch.map(event =>
noteStatsService.fetchNoteStats(event, undefined).catch(() => {})
))
if (i + batchSize < eventsNeedingStats.length) {
await new Promise(resolve => setTimeout(resolve, 200))
}
}
}
// Score events
const scoredEvents = topLevelEvents.map((event) => {
const stats = noteStatsService.getNoteStats(event.id)
let score = 0
if (stats?.likes) score += stats.likes.length
if (stats?.zaps) score += stats.zaps.length
if (stats?.replies) score += stats.replies.length * 3
if (stats?.reposts) score += stats.reposts.length * 5
if (stats?.quotes) score += stats.quotes.length * 8
if (stats?.highlights) score += stats.highlights.length * 10
return { event, score }
})
// Update cache
cachedCustomEvents = {
events: scoredEvents,
timestamp: now,
hashtags: [], // Will be populated when we add hashtags support
listEventIds: [] // Will be populated when we add bookmarks/pins support
}
console.log('[TrendingNotes] Cache initialized with', scoredEvents.length, 'events')
} catch (error) {
console.error('[TrendingNotes] Error initializing cache:', error)
}
}
initializeCache()
}, [getRelays])
const filteredEvents = useMemo(() => {
const idSet = new Set<string>()

56
src/pages/secondary/NoteListPage/index.tsx

@ -8,16 +8,18 @@ import { toProfileList } from '@/lib/link' @@ -8,16 +8,18 @@ import { toProfileList } from '@/lib/link'
import { fetchPubkeysFromDomain, getWellKnownNip05Url } from '@/lib/nip05'
import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import { useInterestList } from '@/providers/InterestListProvider'
import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types'
import { UserRound } from 'lucide-react'
import React, { forwardRef, useEffect, useState } from 'react'
import { UserRound, Plus } from 'lucide-react'
import React, { forwardRef, useEffect, useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
const NoteListPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { relayList, pubkey } = useNostr()
const { isSubscribed, subscribe } = useInterestList()
const [title, setTitle] = useState<React.ReactNode>(null)
const [controls, setControls] = useState<React.ReactNode>(null)
const [data, setData] = useState<
@ -34,6 +36,27 @@ const NoteListPage = forwardRef(({ index, hideTitlebar = false }: { index?: numb @@ -34,6 +36,27 @@ const NoteListPage = forwardRef(({ index, hideTitlebar = false }: { index?: numb
>(null)
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
// Get hashtag from URL if this is a hashtag page
const hashtag = useMemo(() => {
if (data?.type === 'hashtag') {
const searchParams = new URLSearchParams(window.location.search)
return searchParams.get('t')
}
return null
}, [data])
// Check if the hashtag is already in the user's interest list
const isHashtagSubscribed = useMemo(() => {
if (!hashtag) return false
return isSubscribed(hashtag)
}, [hashtag, isSubscribed])
// Add hashtag to interest list
const handleSubscribeHashtag = async () => {
if (!hashtag) return
await subscribe(hashtag)
}
useEffect(() => {
const init = async () => {
const searchParams = new URLSearchParams(window.location.search)
@ -51,6 +74,19 @@ const NoteListPage = forwardRef(({ index, hideTitlebar = false }: { index?: numb @@ -51,6 +74,19 @@ const NoteListPage = forwardRef(({ index, hideTitlebar = false }: { index?: numb
urls: BIG_RELAY_URLS
}
])
// Set controls for hashtag subscribe button
if (pubkey) {
setControls(
<Button
variant="ghost"
className="h-10 [&_svg]:size-3"
onClick={handleSubscribeHashtag}
disabled={isHashtagSubscribed}
>
{isHashtagSubscribed ? t('Subscribed') : t('Subscribe')} <Plus />
</Button>
)
}
return
}
const search = searchParams.get('s')
@ -113,6 +149,22 @@ const NoteListPage = forwardRef(({ index, hideTitlebar = false }: { index?: numb @@ -113,6 +149,22 @@ const NoteListPage = forwardRef(({ index, hideTitlebar = false }: { index?: numb
init()
}, [])
// Update controls when subscription status changes
useEffect(() => {
if (data?.type === 'hashtag' && pubkey) {
setControls(
<Button
variant="ghost"
className="h-10 [&_svg]:size-3"
onClick={handleSubscribeHashtag}
disabled={isHashtagSubscribed}
>
{isHashtagSubscribed ? t('Subscribed') : t('Subscribe')} <Plus />
</Button>
)
}
}, [data, pubkey, isHashtagSubscribed, handleSubscribeHashtag, t])
let content: React.ReactNode = null
if (data?.type === 'domain' && subRequests.length === 0) {
content = (

8
src/providers/NostrProvider/index.tsx

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import LoginDialog from '@/components/LoginDialog'
import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind, PROFILE_FETCH_RELAY_URLS } from '@/constants'
import {
createDeletionRequestDraftEvent,
createFollowListDraftEvent,
@ -298,10 +298,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -298,10 +298,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
// for better performance and accuracy
const normalizedRelays = [
...relayList.write.map(url => normalizeUrl(url) || url),
...BIG_RELAY_URLS.map(url => normalizeUrl(url) || url)
...relayList.write.map((url: string) => normalizeUrl(url) || url),
...PROFILE_FETCH_RELAY_URLS.map((url: string) => normalizeUrl(url) || url)
]
const fetchRelays = Array.from(new Set(normalizedRelays)).slice(0, 4)
const fetchRelays = Array.from(new Set(normalizedRelays)).slice(0, 8)
const events = await client.fetchEvents(fetchRelays, [
{
kinds: [

4
src/services/client.service.ts

@ -1782,6 +1782,10 @@ class ClientService extends EventTarget { @@ -1782,6 +1782,10 @@ class ClientService extends EventTarget {
return this.fetchReplaceableEvent(pubkey, 10015)
}
async fetchPinListEvent(pubkey: string) {
return this.fetchReplaceableEvent(pubkey, 10001)
}
async fetchBlossomServerListEvent(pubkey: string) {
return await this.fetchReplaceableEvent(pubkey, ExtendedKind.BLOSSOM_SERVER_LIST)
}

8
src/services/indexed-db.service.ts

@ -15,6 +15,7 @@ const StoreNames = { @@ -15,6 +15,7 @@ const StoreNames = {
FOLLOW_LIST_EVENTS: 'followListEvents',
MUTE_LIST_EVENTS: 'muteListEvents',
BOOKMARK_LIST_EVENTS: 'bookmarkListEvents',
PIN_LIST_EVENTS: 'pinListEvents',
BLOSSOM_SERVER_LIST_EVENTS: 'blossomServerListEvents',
INTEREST_LIST_EVENTS: 'interestListEvents',
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
@ -44,7 +45,7 @@ class IndexedDbService { @@ -44,7 +45,7 @@ class IndexedDbService {
init(): Promise<void> {
if (!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => {
const request = window.indexedDB.open('jumble', 10)
const request = window.indexedDB.open('jumble', 11)
request.onerror = (event) => {
reject(event)
@ -72,6 +73,9 @@ class IndexedDbService { @@ -72,6 +73,9 @@ class IndexedDbService {
if (!db.objectStoreNames.contains(StoreNames.BOOKMARK_LIST_EVENTS)) {
db.createObjectStore(StoreNames.BOOKMARK_LIST_EVENTS, { keyPath: 'key' })
}
if (!db.objectStoreNames.contains(StoreNames.PIN_LIST_EVENTS)) {
db.createObjectStore(StoreNames.PIN_LIST_EVENTS, { keyPath: 'key' })
}
if (!db.objectStoreNames.contains(StoreNames.INTEREST_LIST_EVENTS)) {
db.createObjectStore(StoreNames.INTEREST_LIST_EVENTS, { keyPath: 'key' })
}
@ -457,6 +461,8 @@ class IndexedDbService { @@ -457,6 +461,8 @@ class IndexedDbService {
return StoreNames.MUTE_LIST_EVENTS
case kinds.BookmarkList:
return StoreNames.BOOKMARK_LIST_EVENTS
case 10001: // Pin list
return StoreNames.PIN_LIST_EVENTS
case 10015: // Interest list
return StoreNames.INTEREST_LIST_EVENTS
case ExtendedKind.BLOSSOM_SERVER_LIST:

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

@ -16,6 +16,10 @@ export type TNoteStats = { @@ -16,6 +16,10 @@ export type TNoteStats = {
zaps: { pr: string; pubkey: string; amount: number; created_at: number; comment?: string }[]
replyIdSet: Set<string>
replies: { id: string; pubkey: string; created_at: number }[]
quoteIdSet: Set<string>
quotes: { id: string; pubkey: string; created_at: number }[]
highlightIdSet: Set<string>
highlights: { id: string; pubkey: string; created_at: number }[]
updatedAt?: number
}
@ -62,6 +66,16 @@ class NoteStatsService { @@ -62,6 +66,16 @@ class NoteStatsService {
'#e': [event.id],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 500
},
{
'#q': [event.id],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 500
},
{
'#e': [event.id],
kinds: [kinds.Highlights],
limit: 500
}
]
@ -81,6 +95,16 @@ class NoteStatsService { @@ -81,6 +95,16 @@ class NoteStatsService {
'#a': [replaceableCoordinate],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 500
},
{
'#q': [replaceableCoordinate],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 500
},
{
'#a': [replaceableCoordinate],
kinds: [kinds.Highlights],
limit: 500
}
)
}
@ -210,8 +234,16 @@ class NoteStatsService { @@ -210,8 +234,16 @@ class NoteStatsService {
} else if (evt.kind === kinds.Zap) {
updatedEventId = this.addZapByEvent(evt)
} else if (evt.kind === kinds.ShortTextNote || evt.kind === ExtendedKind.COMMENT || evt.kind === ExtendedKind.VOICE_COMMENT) {
// Check if it's a reply or quote
const isQuote = this.isQuoteByEvent(evt)
if (isQuote) {
updatedEventId = this.addQuoteByEvent(evt)
} else {
updatedEventId = this.addReplyByEvent(evt)
}
} else if (evt.kind === kinds.Highlights) {
updatedEventId = this.addHighlightByEvent(evt)
}
if (updatedEventId) {
updatedEventIdSet.add(updatedEventId)
}
@ -347,6 +379,45 @@ class NoteStatsService { @@ -347,6 +379,45 @@ class NoteStatsService {
return originalEventId
}
private isQuoteByEvent(evt: Event): boolean {
// A quote has a 'q' tag (quoted event)
return evt.tags.some(tag => tag[0] === 'q' && tag[1])
}
private addQuoteByEvent(evt: Event) {
// Find the quoted event ID from 'q' tag
const quotedEventId = evt.tags.find(tag => tag[0] === 'q')?.[1]
if (!quotedEventId) return
const old = this.noteStatsMap.get(quotedEventId) || {}
const quoteIdSet = old.quoteIdSet || new Set()
const quotes = old.quotes || []
if (quoteIdSet.has(evt.id)) return
quoteIdSet.add(evt.id)
quotes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
this.noteStatsMap.set(quotedEventId, { ...old, quoteIdSet, quotes })
return quotedEventId
}
private addHighlightByEvent(evt: Event) {
// Find the event ID from 'e' tag
const highlightedEventId = evt.tags.find(tag => tag[0] === 'e')?.[1]
if (!highlightedEventId) return
const old = this.noteStatsMap.get(highlightedEventId) || {}
const highlightIdSet = old.highlightIdSet || new Set()
const highlights = old.highlights || []
if (highlightIdSet.has(evt.id)) return
highlightIdSet.add(evt.id)
highlights.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
this.noteStatsMap.set(highlightedEventId, { ...old, highlightIdSet, highlights })
return highlightedEventId
}
private getEmbeddedNoteBech32Ids(event: Event): string[] {
// Simple implementation - in practice, this should match the logic in lib/event.ts
const embeddedIds: string[] = []

Loading…
Cancel
Save