Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
da583a4016
  1. 23
      src/components/RssFeedList/index.tsx
  2. 18
      src/components/RssUrlThreadEventsPreview/index.tsx
  3. 4
      src/components/RssUrlThreadStatsBar/index.tsx
  4. 40
      src/hooks/useRssUrlThreadQueryRelays.ts
  5. 27
      src/lib/rss-article.ts
  6. 28
      src/lib/rss-web-feed.ts
  7. 70
      src/services/note-stats.service.ts

23
src/components/RssFeedList/index.tsx

@ -654,15 +654,10 @@ export default function RssFeedList() { @@ -654,15 +654,10 @@ export default function RssFeedList() {
})
}, [combinedFeedRows, searchQuery, rssItemMatchesSearch])
/** Canonical URLs we know from Nostr (relay discovery or user-added), not RSS-only grouping. */
const urlKeysWithNostrFootprint = useMemo(() => {
const s = new Set<string>()
for (const e of manualWebEntries) s.add(e.url)
for (const e of relayDiscoveredUrls) s.add(e.url)
return s
}, [manualWebEntries, relayDiscoveredUrls])
/** What to show before “only my web events” (used for Nostr URL list). */
/**
* URLs-only view: one card per grouped article URL (same rows as Both), including RSS-sourced URLs
* and full `rssItems` for titles/snippets previously RSS-only rows were hidden and `rssItems` was cleared.
*/
const feedDisplayBase = useMemo(():
| { view: 'rss'; items: TRssFeedItem[] }
| { view: 'unified'; rows: UnifiedFeedRow[] } => {
@ -673,16 +668,10 @@ export default function RssFeedList() { @@ -673,16 +668,10 @@ export default function RssFeedList() {
if (feedScope === 'urls') {
const rows: UnifiedFeedRow[] = combinedFeedRowsForSearch
.filter((r): r is Extract<CombinedFeedRow, { kind: 'web' }> => r.kind === 'web')
.filter((r) => {
const hasRss = r.rssItems.length > 0
const hasNostr = urlKeysWithNostrFootprint.has(r.canonicalUrl)
if (hasRss && !hasNostr) return false
return true
})
.map((r) => ({
kind: 'url' as const,
canonicalUrl: r.canonicalUrl,
rssItems: []
rssItems: r.rssItems
}))
return { view: 'unified', rows }
}
@ -697,7 +686,7 @@ export default function RssFeedList() { @@ -697,7 +686,7 @@ export default function RssFeedList() {
: { kind: 'rssEntry' as const, item: r.item }
)
return { view: 'unified', rows }
}, [feedScope, rssScopeItems, combinedFeedRowsForSearch, urlKeysWithNostrFootprint])
}, [feedScope, rssScopeItems, combinedFeedRowsForSearch])
const persistSuppressClawstr = useCallback((checked: boolean) => {
rssWebPrefsUserTouchedRef.current = true

18
src/components/RssUrlThreadEventsPreview/index.tsx

@ -1,14 +1,13 @@ @@ -1,14 +1,13 @@
import NoteCard from '@/components/NoteCard'
import { Skeleton } from '@/components/ui/skeleton'
import { FAST_READ_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { useNoteStatsRelayHints } from '@/hooks/useNoteStatsRelayHints'
import { useRssUrlThreadQueryRelays } from '@/hooks/useRssUrlThreadQueryRelays'
import {
buildRssArticleUrlThreadInteractionFilterGroups,
isRssArticleUrlThreadInteraction
} from '@/lib/rss-web-feed'
import { queryService } from '@/services/client.service'
import type { Event } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useState } from 'react'
const PREVIEW_LIMIT = 5
const FETCH_LIMIT = 24
@ -17,11 +16,7 @@ const FETCH_LIMIT = 24 @@ -17,11 +16,7 @@ const FETCH_LIMIT = 24
* Compact Nostr thread rows (comments + highlights) for an article URL card in the RSS+Web feed.
*/
export default function RssUrlThreadEventsPreview({ canonicalUrl }: { canonicalUrl: string }) {
const { relays, key: relayHintsKey } = useNoteStatsRelayHints()
const relayUrls = useMemo(
() => [...new Set([...SEARCHABLE_RELAY_URLS, ...FAST_READ_RELAY_URLS, ...relays])],
[relays]
)
const { relayUrls, key: relayKey } = useRssUrlThreadQueryRelays()
const [events, setEvents] = useState<Event[]>([])
const [loading, setLoading] = useState(true)
@ -37,6 +32,11 @@ export default function RssUrlThreadEventsPreview({ canonicalUrl }: { canonicalU @@ -37,6 +32,11 @@ export default function RssUrlThreadEventsPreview({ canonicalUrl }: { canonicalU
globalTimeout: 26_000,
firstRelayResultGraceMs: false as const
}
if (relayUrls.length === 0) {
return () => {
cancelled = true
}
}
void Promise.all([
nonSocial.length > 0 ? queryService.fetchEvents(relayUrls, nonSocial, fetchOpts) : Promise.resolve([]),
social.length > 0 ? queryService.fetchEvents(relayUrls, social, fetchOpts) : Promise.resolve([])
@ -63,7 +63,7 @@ export default function RssUrlThreadEventsPreview({ canonicalUrl }: { canonicalU @@ -63,7 +63,7 @@ export default function RssUrlThreadEventsPreview({ canonicalUrl }: { canonicalU
return () => {
cancelled = true
}
}, [canonicalUrl, relayHintsKey, relayUrls])
}, [canonicalUrl, relayKey, relayUrls])
if (loading) {
return (

4
src/components/RssUrlThreadStatsBar/index.tsx

@ -2,7 +2,7 @@ import { useNoteStatsById } from '@/hooks/useNoteStatsById' @@ -2,7 +2,7 @@ import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { useUserTrust } from '@/contexts/user-trust-context'
import { cn } from '@/lib/utils'
import noteStatsService from '@/services/note-stats.service'
import { useNoteStatsRelayHints } from '@/hooks/useNoteStatsRelayHints'
import { useRssUrlThreadQueryRelays } from '@/hooks/useRssUrlThreadQueryRelays'
import { useNostr } from '@/providers/NostrProvider'
import { Bookmark, Highlighter, MessageCircle, ThumbsUp } from 'lucide-react'
import type { Event } from 'nostr-tools'
@ -18,7 +18,7 @@ export default function RssUrlThreadStatsBar({ @@ -18,7 +18,7 @@ export default function RssUrlThreadStatsBar({
}) {
const { t } = useTranslation()
const { pubkey } = useNostr()
const { relays: statsRelays, key: statsRelaysKey } = useNoteStatsRelayHints()
const { relayUrls: statsRelays, key: statsRelaysKey } = useRssUrlThreadQueryRelays()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const noteStats = useNoteStatsById(event.id)
const [loading, setLoading] = useState(false)

40
src/hooks/useRssUrlThreadQueryRelays.ts

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
import { FAST_READ_RELAY_URLS } from '@/constants'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import { buildRssWebNostrQueryRelayUrls } from '@/lib/rss-web-feed'
import { useEffect, useMemo, useState } from 'react'
import { useNoteStatsRelayHints } from './useNoteStatsRelayHints'
/**
* Relay set for RSS+Web article URL thread REQs: inbox/favorites/fast-read merge (same as URL discovery)
* plus {@link useNoteStatsRelayHints} (current relay context).
*/
export function useRssUrlThreadQueryRelays(): { relayUrls: string[]; key: string } {
const { pubkey } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { relays: hintRelays, key: hintKey } = useNoteStatsRelayHints()
const [baseUrls, setBaseUrls] = useState<string[]>([])
const [baseKey, setBaseKey] = useState('')
useEffect(() => {
let cancelled = false
void buildRssWebNostrQueryRelayUrls({
accountPubkey: pubkey,
favoriteRelays: favoriteRelays ?? [],
blockedRelays: blockedRelays ?? []
}).then((urls) => {
if (cancelled) return
setBaseUrls(urls)
setBaseKey(urls.join('|'))
})
return () => {
cancelled = true
}
}, [pubkey, favoriteRelays, blockedRelays])
return useMemo(() => {
const merged = [...new Set([...baseUrls, ...hintRelays])]
const relayUrls = merged.length > 0 ? merged : [...FAST_READ_RELAY_URLS]
return { relayUrls, key: `${baseKey}::${hintKey}::${relayUrls.length}` }
}, [baseUrls, baseKey, hintRelays, hintKey])
}

27
src/lib/rss-article.ts

@ -120,10 +120,10 @@ export function getReactionPageUrlFromRTags(event: Pick<Event, 'kind' | 'tags'>) @@ -120,10 +120,10 @@ export function getReactionPageUrlFromRTags(event: Pick<Event, 'kind' | 'tags'>)
}
/**
* Values for a REQ `#r` filter on kind 9802 when the thread key is a canonical article URL.
* Relay matching is exact on the tag string, so we include common variants (slash, stripped query).
* Canonical article URL plus common string variants for REQ filters (`i` / `I` / `r`).
* Relay matching is exact on tag values, so trailing slashes, query stripping, etc. are included.
*/
export function computeRTagFilterValuesForArticleThread(canonicalUrl: string): string[] {
export function expandArticleUrlThreadQueryValues(canonicalUrl: string): string[] {
const s = canonicalUrl.trim()
if (!s.startsWith('http://') && !s.startsWith('https://')) return []
const out = new Set<string>([s])
@ -144,6 +144,27 @@ export function computeRTagFilterValuesForArticleThread(canonicalUrl: string): s @@ -144,6 +144,27 @@ export function computeRTagFilterValuesForArticleThread(canonicalUrl: string): s
return [...out]
}
/**
* Values for a REQ `#r` filter on kind 9802 / kind 7 when the thread key is a canonical article URL.
* @deprecated Prefer {@link expandArticleUrlThreadQueryValues} same values.
*/
export function computeRTagFilterValuesForArticleThread(canonicalUrl: string): string[] {
return expandArticleUrlThreadQueryValues(canonicalUrl)
}
/** True if `urlFromEvent` refers to the same article as `canonicalThreadKey` (after normalization + variant match). */
export function articleUrlMatchesThreadScope(urlFromEvent: string, canonicalThreadKey: string): boolean {
const key = canonicalizeRssArticleUrl(canonicalThreadKey)
const cand = canonicalizeRssArticleUrl(urlFromEvent)
if (key === cand) return true
const keyVariants = new Set(expandArticleUrlThreadQueryValues(key))
if (keyVariants.has(cand)) return true
for (const v of expandArticleUrlThreadQueryValues(cand)) {
if (keyVariants.has(v)) return true
}
return false
}
/** True for http(s) URLs whose host is clawstr.com (incl. subdomains; supports protocol-relative `//…`). */
export function isClawstrDotComHttpUrl(url: string): boolean {
const t = url.trim()

28
src/lib/rss-web-feed.ts

@ -3,8 +3,9 @@ import { buildAccountListRelayUrlsForMerge } from '@/lib/account-list-relay-urls @@ -3,8 +3,9 @@ import { buildAccountListRelayUrlsForMerge } from '@/lib/account-list-relay-urls
import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays'
import { isReplyNoteEvent } from '@/lib/event'
import {
articleUrlMatchesThreadScope,
canonicalizeRssArticleUrl,
computeRTagFilterValuesForArticleThread,
expandArticleUrlThreadQueryValues,
getArticleUrlFromCommentITags,
getHighlightSourceHttpUrl,
getReactionPageUrlFromRTags,
@ -183,19 +184,20 @@ export function buildRssArticleUrlThreadInteractionFilterGroups( @@ -183,19 +184,20 @@ export function buildRssArticleUrlThreadInteractionFilterGroups(
limit: number
): { nonSocial: Filter[]; social: Filter[] } {
const canonical = canonicalizeRssArticleUrl(canonicalArticleUrl)
const rVals = computeRTagFilterValuesForArticleThread(canonical)
const tagVals = expandArticleUrlThreadQueryValues(canonical)
const iFilterVals = tagVals.length > 0 ? tagVals : [canonical]
const social: Filter[] = [
{ '#i': [canonical], kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], limit },
{ '#I': [canonical], kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], limit }
{ '#i': iFilterVals, kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], limit },
{ '#I': iFilterVals, kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], limit }
]
const nonSocial: Filter[] = [
{ '#i': [canonical], kinds: [ExtendedKind.EXTERNAL_REACTION], limit },
{ '#I': [canonical], kinds: [ExtendedKind.EXTERNAL_REACTION], limit }
{ '#i': iFilterVals, kinds: [ExtendedKind.EXTERNAL_REACTION], limit },
{ '#I': iFilterVals, kinds: [ExtendedKind.EXTERNAL_REACTION], limit }
]
if (rVals.length > 0) {
if (tagVals.length > 0) {
nonSocial.push(
{ '#r': rVals, kinds: [kinds.Highlights], limit },
{ '#r': rVals, kinds: [kinds.Reaction], limit }
{ '#r': tagVals, kinds: [kinds.Highlights], limit },
{ '#r': tagVals, kinds: [kinds.Reaction], limit }
)
}
return { nonSocial, social }
@ -218,19 +220,19 @@ export function isRssArticleUrlThreadInteraction(evt: Event, canonicalArticleUrl @@ -218,19 +220,19 @@ export function isRssArticleUrlThreadInteraction(evt: Event, canonicalArticleUrl
const key = canonicalizeRssArticleUrl(canonicalArticleUrl)
if (evt.kind === kinds.Highlights) {
const hu = getHighlightSourceHttpUrl(evt)
return !!hu && canonicalizeRssArticleUrl(hu) === key
return !!hu && articleUrlMatchesThreadScope(hu, key)
}
if (evt.kind === ExtendedKind.EXTERNAL_REACTION) {
const u = getWebExternalReactionTargetUrl(evt)
return !!u && canonicalizeRssArticleUrl(u) === key
return !!u && articleUrlMatchesThreadScope(u, key)
}
if (evt.kind === kinds.Reaction) {
const u = getReactionPageUrlFromRTags(evt)
return !!u && canonicalizeRssArticleUrl(u) === key
return !!u && articleUrlMatchesThreadScope(u, key)
}
if (!isReplyNoteEvent(evt)) return false
const u = getArticleUrlFromCommentITags(evt)
return !!u && canonicalizeRssArticleUrl(u) === key
return !!u && articleUrlMatchesThreadScope(u, key)
}
/**

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

@ -10,10 +10,11 @@ import { getZapInfoFromEvent } from '@/lib/event-metadata' @@ -10,10 +10,11 @@ import { getZapInfoFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import {
canonicalizeRssArticleUrl,
computeRTagFilterValuesForArticleThread,
expandArticleUrlThreadQueryValues,
getArticleUrlFromCommentITags,
getHighlightSourceHttpUrl,
getReactionPageUrlFromRTags,
getWebBookmarkArticleUrl,
getWebExternalReactionTargetUrl,
rssArticleStableEventId
} from '@/lib/rss-article'
@ -288,6 +289,34 @@ class NoteStatsService { @@ -288,6 +289,34 @@ class NoteStatsService {
const reactionLimit = 300
const interactionLimit = 80
/** 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) {
const url = getArticleUrlFromCommentITags(event)
if (!url) {
return { nonSocial: [], social: [] }
}
const canonical = canonicalizeRssArticleUrl(url)
const tagVals = expandArticleUrlThreadQueryValues(canonical)
const iVals = tagVals.length > 0 ? tagVals : [canonical]
const nonSocial: Filter[] = [
{ '#i': iVals, kinds: [ExtendedKind.EXTERNAL_REACTION], limit: reactionLimit },
{ '#I': iVals, kinds: [ExtendedKind.EXTERNAL_REACTION], limit: reactionLimit },
{ '#i': iVals, kinds: [ExtendedKind.WEB_BOOKMARK], limit: 200 },
{ '#I': iVals, kinds: [ExtendedKind.WEB_BOOKMARK], limit: 200 }
]
if (tagVals.length > 0) {
nonSocial.push(
{ '#r': tagVals, kinds: [kinds.Highlights], limit: interactionLimit },
{ '#r': tagVals, kinds: [kinds.Reaction], limit: reactionLimit }
)
}
const social: Filter[] = [
{ '#i': iVals, kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], limit: interactionLimit },
{ '#I': iVals, kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], limit: interactionLimit }
]
return { nonSocial, social }
}
const nonSocial: Filter[] = [
{ '#e': [event.id], kinds: [kinds.Reaction], limit: reactionLimit },
{ '#e': [event.id], kinds: [kinds.Zap], limit: 100 }
@ -312,29 +341,6 @@ class NoteStatsService { @@ -312,29 +341,6 @@ class NoteStatsService {
}
]
if (event.kind === ExtendedKind.RSS_THREAD_ROOT) {
const url = getArticleUrlFromCommentITags(event)
if (url) {
const canonical = canonicalizeRssArticleUrl(url)
const rVals = computeRTagFilterValuesForArticleThread(canonical)
nonSocial.push(
{ '#i': [canonical], kinds: [ExtendedKind.EXTERNAL_REACTION], limit: reactionLimit },
{ '#I': [canonical], kinds: [ExtendedKind.EXTERNAL_REACTION], limit: reactionLimit },
{ kinds: [kinds.BookmarkList], '#e': [event.id], limit: 200 }
)
if (rVals.length > 0) {
nonSocial.push(
{ '#r': rVals, kinds: [kinds.Highlights], limit: interactionLimit },
{ '#r': rVals, kinds: [kinds.Reaction], limit: reactionLimit }
)
}
social.push(
{ '#i': [canonical], kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], limit: interactionLimit },
{ '#I': [canonical], kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], limit: interactionLimit }
)
}
}
if (replaceableCoordinate) {
nonSocial.push(
{ '#a': [replaceableCoordinate], kinds: [kinds.Reaction], limit: reactionLimit },
@ -472,6 +478,8 @@ class NoteStatsService { @@ -472,6 +478,8 @@ class NoteStatsService {
}
} else if (evt.kind === kinds.Highlights) {
updatedEventId = this.addHighlightByEvent(evt, originalEventAuthor)
} else if (evt.kind === ExtendedKind.WEB_BOOKMARK) {
updatedEventId = this.addWebBookmarkByArticleUrlEvent(evt)
} else if (evt.kind === kinds.BookmarkList) {
this.addBookmarkListRefsByEvent(evt)
}
@ -728,6 +736,20 @@ class NoteStatsService { @@ -728,6 +736,20 @@ class NoteStatsService {
return highlightedEventId
}
/** Kind 39701: count one bookmark per pubkey for this article URL (synthetic thread id). */
private addWebBookmarkByArticleUrlEvent(evt: Event): string | undefined {
const url = getWebBookmarkArticleUrl(evt)
if (!url) return
const targetId = rssArticleStableEventId(canonicalizeRssArticleUrl(url))
const old = this.noteStatsMap.get(targetId) || {}
const bookmarkPubkeySet = old.bookmarkPubkeySet ?? new Set<string>()
if (bookmarkPubkeySet.has(evt.pubkey)) return targetId
bookmarkPubkeySet.add(evt.pubkey)
this.noteStatsMap.set(targetId, { ...old, bookmarkPubkeySet })
this.notifyNoteStats(targetId)
return targetId
}
/** Each bookmark list author counts once per target `e` id in that list. */
private addBookmarkListRefsByEvent(evt: Event) {
for (const tag of evt.tags) {

Loading…
Cancel
Save