Browse Source

bug-fixes

imwald
Silberengel 2 weeks ago
parent
commit
2eeb5ee797
  1. 17
      src/lib/mute-set.test.ts
  2. 19
      src/lib/mute-set.ts
  3. 7
      src/lib/relay-thread-heat-cache.ts
  4. 35
      src/pages/primary/SpellsPage/ProfileInteractionsMap.test.ts
  5. 74
      src/pages/primary/SpellsPage/ProfileInteractionsMap.tsx
  6. 21
      src/pages/primary/SpellsPage/RelayThreadHeatMap.tsx
  7. 21
      src/pages/primary/SpellsPage/TopicKeywordHeatMap.test.ts
  8. 25
      src/pages/primary/SpellsPage/TopicKeywordHeatMap.tsx

17
src/lib/mute-set.test.ts

@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import { kinds } from 'nostr-tools'
import { filterEventsExcludingMutedAuthors } from './mute-set'
describe('filterEventsExcludingMutedAuthors', () => {
it('drops events from muted pubkeys', () => {
const muted = 'a'.repeat(64)
const other = 'b'.repeat(64)
const events = [
{ kind: kinds.ShortTextNote, pubkey: muted, id: '1'.repeat(64), sig: 's', tags: [], content: '', created_at: 1 },
{ kind: kinds.ShortTextNote, pubkey: other, id: '2'.repeat(64), sig: 's', tags: [], content: '', created_at: 1 }
]
const out = filterEventsExcludingMutedAuthors(events, new Set([muted]))
expect(out).toHaveLength(1)
expect(out[0]?.pubkey).toBe(other)
})
})

19
src/lib/mute-set.ts

@ -1,7 +1,24 @@
import type { Event } from 'nostr-tools'
/** /**
* Mute pubkey sets use lowercase hex so lookups match Nostr events and `p` tags regardless of casing. * Mute pubkey sets use lowercase hex so lookups match Nostr events and `p` tags regardless of casing.
*/ */
export function muteSetHas(mutePubkeySet: Set<string>, pubkey: string | undefined | null): boolean { export function muteSetHas(mutePubkeySet: ReadonlySet<string>, pubkey: string | undefined | null): boolean {
if (!pubkey) return false if (!pubkey) return false
return mutePubkeySet.has(pubkey.toLowerCase()) return mutePubkeySet.has(pubkey.toLowerCase())
} }
/** Drop notes whose author is in the viewer's public or private mute list. */
export function filterEventsExcludingMutedAuthors(
events: readonly Event[],
mutePubkeySet: ReadonlySet<string>
): Event[] {
if (mutePubkeySet.size === 0) return [...events]
return events.filter((ev) => !muteSetHas(mutePubkeySet, ev.pubkey))
}
/** Stable SETTINGS / cache segment when mute lists change. */
export function mutePubkeySetFingerprint(mutePubkeySet: ReadonlySet<string>): string {
if (mutePubkeySet.size === 0) return '0'
return [...mutePubkeySet].sort().join('\n')
}

7
src/lib/relay-thread-heat-cache.ts

@ -26,7 +26,9 @@ export function relayThreadHeatMapSettingKey(
relayUrls: readonly string[], relayUrls: readonly string[],
followPubkeys: readonly string[], followPubkeys: readonly string[],
/** Serialized home kind-picker state so cache invalidates when feed filters change. */ /** Serialized home kind-picker state so cache invalidates when feed filters change. */
feedFilterKey: string feedFilterKey: string,
/** Sorted mute pubkeys so cache invalidates when mutes change. */
muteFingerprint: string
): string { ): string {
const pk = pubkey.trim().toLowerCase() const pk = pubkey.trim().toLowerCase()
const relayKey = digestHeatMapKeyPart([...relayUrls].sort().join('\n')) const relayKey = digestHeatMapKeyPart([...relayUrls].sort().join('\n'))
@ -38,7 +40,8 @@ export function relayThreadHeatMapSettingKey(
.join('\n') .join('\n')
) )
const feedKey = digestHeatMapKeyPart(feedFilterKey) const feedKey = digestHeatMapKeyPart(feedFilterKey)
return `relayHeatV${CACHE_V}:${pk}:${relayKey}:${followKey}:${feedKey}` const muteKey = digestHeatMapKeyPart(muteFingerprint)
return `relayHeatV${CACHE_V}:${pk}:${relayKey}:${followKey}:${feedKey}:${muteKey}`
} }
export function parseRelayThreadHeatMapCache(raw: string | null): TRelayThreadHeatMapCacheEnvelope | null { export function parseRelayThreadHeatMapCache(raw: string | null): TRelayThreadHeatMapCacheEnvelope | null {

35
src/pages/primary/SpellsPage/ProfileInteractionsMap.test.ts

@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest'
import { kinds } from 'nostr-tools'
import { mergeInteractionEvents } from './ProfileInteractionsMap'
function interaction(pubkey: string, pTags: string[]) {
return {
kind: kinds.ShortTextNote,
pubkey,
tags: pTags.map((p) => ['p', p]),
content: '',
id: `${pubkey.slice(0, 8)}${'c'.repeat(56)}`,
sig: 's'.repeat(128),
created_at: 1_700_000_000
}
}
describe('mergeInteractionEvents', () => {
it('excludes muted partners and events authored by muted pubkeys', () => {
const profile = 'a'.repeat(64)
const partner = 'b'.repeat(64)
const muted = 'f'.repeat(64)
const cards = mergeInteractionEvents(
profile,
[
interaction(profile, [partner]),
interaction(profile, [muted]),
interaction(muted, [profile]),
interaction(partner, [profile])
],
new Set([muted])
)
expect(cards.map((c) => c.pubkey)).toEqual([partner])
expect(cards[0]?.score).toBe(2)
})
})

74
src/pages/primary/SpellsPage/ProfileInteractionsMap.tsx

@ -4,7 +4,9 @@ import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { useMuteList } from '@/contexts/mute-list-context'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays' import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { muteSetHas } from '@/lib/mute-set'
import { toProfile } from '@/lib/link' import { toProfile } from '@/lib/link'
import { formatPubkey } from '@/lib/pubkey' import { formatPubkey } from '@/lib/pubkey'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -57,12 +59,18 @@ function interactionFilters(pubkey: string, limit: number): TSubRequestFilter[]
] ]
} }
function mergeInteractionEvents(targetPubkey: string, events: Event[]): InteractionCard[] { export function mergeInteractionEvents(
targetPubkey: string,
events: Event[],
mutePubkeySet: ReadonlySet<string>
): InteractionCard[] {
const target = targetPubkey.toLowerCase() const target = targetPubkey.toLowerCase()
const byPubkey = new Map<string, InteractionCard>() const byPubkey = new Map<string, InteractionCard>()
const add = (partnerRaw: string | undefined, event: Event, direction: 'out' | 'in') => { const add = (partnerRaw: string | undefined, event: Event, direction: 'out' | 'in') => {
if (muteSetHas(mutePubkeySet, event.pubkey)) return
const partner = partnerRaw?.trim().toLowerCase() const partner = partnerRaw?.trim().toLowerCase()
if (!partner || partner === target || !/^[0-9a-f]{64}$/.test(partner)) return if (!partner || partner === target || !/^[0-9a-f]{64}$/.test(partner)) return
if (muteSetHas(mutePubkeySet, partner)) return
let row = byPubkey.get(partner) let row = byPubkey.get(partner)
if (!row) { if (!row) {
row = { row = {
@ -112,6 +120,7 @@ export default function ProfileInteractionsMap({ pubkey, refreshKey }: Props) {
const { t } = useTranslation() const { t } = useTranslation()
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const { relayList, cacheRelayListEvent } = useNostr() const { relayList, cacheRelayListEvent } = useNostr()
const { mutePubkeySet } = useMuteList()
const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const [cards, setCards] = useState<InteractionCard[]>([]) const [cards, setCards] = useState<InteractionCard[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@ -168,12 +177,12 @@ export default function ProfileInteractionsMap({ pubkey, refreshKey }: Props) {
try { try {
const local = await load(false) const local = await load(false)
if (cancelled) return if (cancelled) return
setCards(mergeInteractionEvents(pubkey, local)) setCards(mergeInteractionEvents(pubkey, local, mutePubkeySet))
setLoading(false) setLoading(false)
const all = await load(true) const all = await load(true)
if (cancelled) return if (cancelled) return
setCards(mergeInteractionEvents(pubkey, all)) setCards(mergeInteractionEvents(pubkey, all, mutePubkeySet))
} catch (e) { } catch (e) {
if (cancelled) return if (cancelled) return
setError(e instanceof Error ? e.message : String(e)) setError(e instanceof Error ? e.message : String(e))
@ -187,7 +196,7 @@ export default function ProfileInteractionsMap({ pubkey, refreshKey }: Props) {
return () => { return () => {
cancelled = true cancelled = true
} }
}, [pubkey, refreshKey, load]) }, [pubkey, refreshKey, load, mutePubkeySet])
return ( return (
<div className="flex min-h-0 flex-1 flex-col gap-4"> <div className="flex min-h-0 flex-1 flex-col gap-4">
@ -203,7 +212,7 @@ export default function ProfileInteractionsMap({ pubkey, refreshKey }: Props) {
onClick={() => { onClick={() => {
setRefreshing(true) setRefreshing(true)
void load(true) void load(true)
.then((rows) => setCards(mergeInteractionEvents(pubkey, rows))) .then((rows) => setCards(mergeInteractionEvents(pubkey, rows, mutePubkeySet)))
.catch((e) => setError(e instanceof Error ? e.message : String(e))) .catch((e) => setError(e instanceof Error ? e.message : String(e)))
.finally(() => setRefreshing(false)) .finally(() => setRefreshing(false))
}} }}
@ -221,7 +230,7 @@ export default function ProfileInteractionsMap({ pubkey, refreshKey }: Props) {
{loading && cards.length === 0 ? ( {loading && cards.length === 0 ? (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
{Array.from({ length: 9 }).map((_, i) => ( {Array.from({ length: 9 }).map((_, i) => (
<Skeleton key={i} className="h-24 rounded-xl" /> <Skeleton key={i} className="h-44 rounded-xl" />
))} ))}
</div> </div>
) : error && cards.length === 0 ? ( ) : error && cards.length === 0 ? (
@ -234,7 +243,7 @@ export default function ProfileInteractionsMap({ pubkey, refreshKey }: Props) {
</div> </div>
) : ( ) : (
<div className="min-h-0 flex-1 overflow-y-auto pb-4"> <div className="min-h-0 flex-1 overflow-y-auto pb-4">
<div className="grid grid-cols-1 gap-2 min-[720px]:grid-cols-2 xl:grid-cols-3"> <div className="grid grid-cols-1 gap-3 min-[720px]:grid-cols-2 xl:grid-cols-3">
{cards.map((card, index) => ( {cards.map((card, index) => (
<button <button
key={card.pubkey} key={card.pubkey}
@ -244,38 +253,39 @@ export default function ProfileInteractionsMap({ pubkey, refreshKey }: Props) {
> >
<Card <Card
className={cn( className={cn(
'flex h-full min-w-0 items-center gap-2 p-2 transition-colors hover:bg-accent/70 min-[720px]:gap-3 min-[720px]:p-3', 'flex h-full min-w-0 flex-col overflow-hidden p-3 transition-colors hover:bg-accent/70',
index < 3 && 'border-primary/40 bg-primary/5' index < 3 && 'border-primary/40 bg-primary/5'
)} )}
> >
<div className="relative shrink-0"> <div className="flex min-w-0 items-start justify-between gap-2">
<UserAvatar userId={card.pubkey} size="semiBig" className="min-[720px]:h-16 min-[720px]:w-16" /> <div className="min-w-0 flex-1">
<span className="absolute -bottom-1 -right-1 z-10 rounded-full bg-background px-1.5 py-0.5 text-[10px] font-semibold shadow ring-1 ring-border"> <Username userId={card.pubkey} className="block truncate text-sm font-semibold" />
<div className="truncate text-xs text-muted-foreground">{formatPubkey(card.pubkey)}</div>
</div>
<span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-semibold text-foreground ring-1 ring-border">
#{index + 1} #{index + 1}
</span> </span>
</div> </div>
<div className="min-w-0 flex-1">
<Username userId={card.pubkey} className="block truncate text-sm font-semibold" /> <div className="mt-2 flex min-w-0 flex-wrap gap-1.5 text-[11px] text-muted-foreground">
<div className="truncate text-xs text-muted-foreground">{formatPubkey(card.pubkey)}</div> <span className="max-w-full truncate rounded-full bg-muted px-2 py-0.5 font-medium text-foreground">
<div className="mt-1.5 flex min-w-0 flex-wrap gap-1 text-[11px] text-muted-foreground min-[720px]:mt-2 min-[720px]:gap-1.5 min-[720px]:text-xs"> <UserRound className="mr-1 inline size-3 shrink-0" aria-hidden />
<span className="rounded-full bg-muted px-2 py-0.5 font-medium text-foreground"> {t('n interactions', { count: card.score, formattedCount: compactCount(card.score) })}
<UserRound className="mr-1 inline size-3" aria-hidden /> </span>
<span className="min-[720px]:hidden">{compactCount(card.score)}</span> {card.authoredByProfile > 0 ? (
<span className="hidden min-[720px]:inline"> <span className="max-w-full truncate rounded-full bg-muted px-2 py-0.5">
{t('n interactions', { count: card.score, formattedCount: compactCount(card.score) })} {t('outgoing interactions', { count: card.authoredByProfile })}
</span>
</span> </span>
{card.authoredByProfile > 0 ? ( ) : null}
<span className="hidden rounded-full bg-muted px-2 py-0.5 min-[720px]:inline"> {card.mentionsProfile > 0 ? (
{t('outgoing interactions', { count: card.authoredByProfile })} <span className="max-w-full truncate rounded-full bg-muted px-2 py-0.5">
</span> {t('incoming interactions', { count: card.mentionsProfile })}
) : null} </span>
{card.mentionsProfile > 0 ? ( ) : null}
<span className="hidden rounded-full bg-muted px-2 py-0.5 min-[720px]:inline"> </div>
{t('incoming interactions', { count: card.mentionsProfile })}
</span> <div className="mt-3 flex justify-center">
) : null} <UserAvatar userId={card.pubkey} size="big" className="size-16 shrink-0 sm:size-20" />
</div>
</div> </div>
</Card> </Card>
</button> </button>

21
src/pages/primary/SpellsPage/RelayThreadHeatMap.tsx

@ -4,6 +4,7 @@ import { SimpleUserAvatar } from '@/components/UserAvatar'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter' import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter'
import { filterEventsExcludingTombstones } from '@/lib/event' import { filterEventsExcludingTombstones } from '@/lib/event'
import { filterEventsExcludingMutedAuthors, mutePubkeySetFingerprint, muteSetHas } from '@/lib/mute-set'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays' import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -22,6 +23,7 @@ import {
type TRelayThreadHeatEdge type TRelayThreadHeatEdge
} from '@/lib/relay-thread-heat' } from '@/lib/relay-thread-heat'
import { usePrimaryPage } from '@/contexts/primary-page-context' import { usePrimaryPage } from '@/contexts/primary-page-context'
import { useMuteList } from '@/contexts/mute-list-context'
import { useSmartNoteNavigation } from '@/PageManager' import { useSmartNoteNavigation } from '@/PageManager'
import { encodeProfileInteractionsSpellId } from './fauxSpellConfig' import { encodeProfileInteractionsSpellId } from './fauxSpellConfig'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@ -101,6 +103,7 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props)
const { navigate: navigatePrimary } = usePrimaryPage() const { navigate: navigatePrimary } = usePrimaryPage()
const { navigateToNote } = useSmartNoteNavigation() const { navigateToNote } = useSmartNoteNavigation()
const { pubkey, relayList, cacheRelayListEvent } = useNostr() const { pubkey, relayList, cacheRelayListEvent } = useNostr()
const { mutePubkeySet } = useMuteList()
const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { showKinds, showKind1OPs, showKind1Replies, showKind1111 } = useKindFilterOrDefaults() const { showKinds, showKind1OPs, showKind1Replies, showKind1111 } = useKindFilterOrDefaults()
@ -142,9 +145,14 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [rescanTick, setRescanTick] = useState(0) const [rescanTick, setRescanTick] = useState(0)
const muteFingerprint = useMemo(() => mutePubkeySetFingerprint(mutePubkeySet), [mutePubkeySet])
const cacheSettingKey = useMemo( const cacheSettingKey = useMemo(
() => (pubkey ? relayThreadHeatMapSettingKey(pubkey, relayUrls, followPubkeys, feedFilterKey) : ''), () =>
[pubkey, relayUrls, followPubkeys, feedFilterKey] pubkey
? relayThreadHeatMapSettingKey(pubkey, relayUrls, followPubkeys, feedFilterKey, muteFingerprint)
: '',
[pubkey, relayUrls, followPubkeys, feedFilterKey, muteFingerprint]
) )
const mergeHeatMapData = useCallback(async (includeRelay = true): Promise<{ const mergeHeatMapData = useCallback(async (includeRelay = true): Promise<{
@ -204,7 +212,10 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props)
dedup.set(ev.id.toLowerCase(), ev) dedup.set(ev.id.toLowerCase(), ev)
} }
} }
const merged = filterEventsExcludingTombstones([...dedup.values()], tombstones) const merged = filterEventsExcludingMutedAuthors(
filterEventsExcludingTombstones([...dedup.values()], tombstones),
mutePubkeySet
)
const feedNotes = merged.filter((e) => const feedNotes = merged.filter((e) =>
eventPassesNoteListKindPicker(e, showKinds, showKind1OPs, showKind1Replies, showKind1111) eventPassesNoteListKindPicker(e, showKinds, showKind1OPs, showKind1Replies, showKind1111)
) )
@ -227,6 +238,7 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props)
for (const ev of archived) { for (const ev of archived) {
if (!verifyEvent(ev)) continue if (!verifyEvent(ev)) continue
if (ev.kind !== kinds.ShortTextNote && ev.kind !== ExtendedKind.DISCUSSION) continue if (ev.kind !== kinds.ShortTextNote && ev.kind !== ExtendedKind.DISCUSSION) continue
if (muteSetHas(mutePubkeySet, ev.pubkey)) continue
rootById.set(ev.id.toLowerCase(), ev) rootById.set(ev.id.toLowerCase(), ev)
} }
const stillMissing = missingRootIds.filter((id) => !rootById.has(id)) const stillMissing = missingRootIds.filter((id) => !rootById.has(id))
@ -244,6 +256,7 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props)
for (const ev of fetched) { for (const ev of fetched) {
if (!verifyEvent(ev)) continue if (!verifyEvent(ev)) continue
if (ev.kind !== kinds.ShortTextNote && ev.kind !== ExtendedKind.DISCUSSION) continue if (ev.kind !== kinds.ShortTextNote && ev.kind !== ExtendedKind.DISCUSSION) continue
if (muteSetHas(mutePubkeySet, ev.pubkey)) continue
rootById.set(ev.id.toLowerCase(), ev) rootById.set(ev.id.toLowerCase(), ev)
} }
} }
@ -270,7 +283,7 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props)
edges: edges.length edges: edges.length
}) })
return { bubbles, edges } return { bubbles, edges }
}, [relayUrls, followSet, showKinds, showKind1OPs, showKind1Replies, showKind1111]) }, [relayUrls, followSet, showKinds, showKind1OPs, showKind1Replies, showKind1111, mutePubkeySet])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false

21
src/pages/primary/SpellsPage/TopicKeywordHeatMap.test.ts

@ -38,4 +38,25 @@ describe('buildTopicKeywordBubbles', () => {
expect(nostr?.pubkeys).toContain(pkC) expect(nostr?.pubkeys).toContain(pkC)
expect(nostr?.pubkeys).toContain(pkB) expect(nostr?.pubkeys).toContain(pkB)
}) })
it('excludes muted authors from counts and bubble avatars', () => {
const pkA = 'a'.repeat(64)
const pkMuted = 'f'.repeat(64)
const pkB = 'b'.repeat(64)
const bubbles = buildTopicKeywordBubbles(
[
note(pkA, [['t', 'nostr']]),
note(pkMuted, [['t', 'nostr']], 'muted #nostr'),
note(pkB, [['t', 'nostr']])
],
DEFAULT_FEED_SHOW_KINDS,
true,
true,
true,
new Set([pkMuted])
)
const nostr = bubbles.find((b) => b.key === 'nostr')
expect(nostr?.score).toBe(2)
expect(nostr?.pubkeys).not.toContain(pkMuted)
})
}) })

25
src/pages/primary/SpellsPage/TopicKeywordHeatMap.tsx

@ -1,7 +1,9 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { useMuteList } from '@/contexts/mute-list-context'
import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter' import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter'
import { filterEventsExcludingMutedAuthors, muteSetHas } from '@/lib/mute-set'
import { filterEventsExcludingTombstones } from '@/lib/event' import { filterEventsExcludingTombstones } from '@/lib/event'
import { extractHashtagsFromContent, formatTopicMapBubbleLabel, isValidNormalizedTopicKey, normalizeTopic } from '@/lib/discussion-topics' import { extractHashtagsFromContent, formatTopicMapBubbleLabel, isValidNormalizedTopicKey, normalizeTopic } from '@/lib/discussion-topics'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays' import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
@ -50,8 +52,13 @@ type TopicKeyAccum = {
pubkeyHits: Map<string, number> pubkeyHits: Map<string, number>
} }
function topPubkeysForTopic(hits: Map<string, number>, limit: number): string[] { function topPubkeysForTopic(
hits: Map<string, number>,
limit: number,
mutePubkeySet?: ReadonlySet<string>
): string[] {
return [...hits.entries()] return [...hits.entries()]
.filter(([pk]) => !mutePubkeySet || !muteSetHas(mutePubkeySet, pk))
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.slice(0, limit) .slice(0, limit)
.map(([pk]) => pk) .map(([pk]) => pk)
@ -148,7 +155,8 @@ export function buildTopicKeywordBubbles(
showKinds: readonly number[], showKinds: readonly number[],
showKind1OPs: boolean, showKind1OPs: boolean,
showKind1Replies: boolean, showKind1Replies: boolean,
showKind1111: boolean showKind1111: boolean,
mutePubkeySet?: ReadonlySet<string>
): TTopicKeywordBubble[] { ): TTopicKeywordBubble[] {
const accum = new Map<string, TopicKeyAccum>() const accum = new Map<string, TopicKeyAccum>()
@ -168,6 +176,7 @@ export function buildTopicKeywordBubbles(
} }
for (const ev of events) { for (const ev of events) {
if (mutePubkeySet && muteSetHas(mutePubkeySet, ev.pubkey)) continue
if (!eventPassesNoteListKindPicker(ev, showKinds, showKind1OPs, showKind1Replies, showKind1111)) continue if (!eventPassesNoteListKindPicker(ev, showKinds, showKind1OPs, showKind1Replies, showKind1111)) continue
const topics = new Set<string>() const topics = new Set<string>()
for (const row of ev.tags) { for (const row of ev.tags) {
@ -192,7 +201,7 @@ export function buildTopicKeywordBubbles(
score, score,
topicNoteCount: row.topicNoteCount, topicNoteCount: row.topicNoteCount,
keywordNoteCount: row.keywordNoteCount, keywordNoteCount: row.keywordNoteCount,
pubkeys: topPubkeysForTopic(row.pubkeyHits, MAX_BUBBLE_AVATARS) pubkeys: topPubkeysForTopic(row.pubkeyHits, MAX_BUBBLE_AVATARS, mutePubkeySet)
}) })
} }
out.sort((x, y) => y.score - x.score || x.key.localeCompare(y.key)) out.sort((x, y) => y.score - x.score || x.key.localeCompare(y.key))
@ -205,6 +214,7 @@ type Props = {
export default function TopicKeywordHeatMap({ refreshKey }: Props) { export default function TopicKeywordHeatMap({ refreshKey }: Props) {
const { t } = useTranslation() const { t } = useTranslation()
const { mutePubkeySet } = useMuteList()
const { navigateToHashtag } = useSmartHashtagNavigation() const { navigateToHashtag } = useSmartHashtagNavigation()
const { relayList, cacheRelayListEvent } = useNostr() const { relayList, cacheRelayListEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { favoriteRelays, blockedRelays } = useFavoriteRelays()
@ -276,9 +286,12 @@ export default function TopicKeywordHeatMap({ refreshKey }: Props) {
dedup.set(ev.id.toLowerCase(), ev) dedup.set(ev.id.toLowerCase(), ev)
} }
} }
const clean = filterEventsExcludingTombstones([...dedup.values()], tombstones) const clean = filterEventsExcludingMutedAuthors(
return buildTopicKeywordBubbles(clean, showKinds, showKind1OPs, showKind1Replies, showKind1111) filterEventsExcludingTombstones([...dedup.values()], tombstones),
}, [relayUrls, showKinds, showKind1OPs, showKind1Replies, showKind1111]) mutePubkeySet
)
return buildTopicKeywordBubbles(clean, showKinds, showKind1OPs, showKind1Replies, showKind1111, mutePubkeySet)
}, [relayUrls, showKinds, showKind1OPs, showKind1Replies, showKind1111, mutePubkeySet])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false

Loading…
Cancel
Save