Browse Source

fix heat map bubbles pointing to thread content instead of OPs

imwald
Silberengel 1 month ago
parent
commit
b6335f6d49
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 8
      src/lib/relay-thread-heat.ts
  4. 54
      src/pages/primary/SpellsPage/RelayThreadHeatMap.tsx

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.6.0", "version": "23.7.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imwald", "name": "imwald",
"version": "23.6.0", "version": "23.7.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.6.0", "version": "23.7.0",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

8
src/lib/relay-thread-heat.ts

@ -26,7 +26,7 @@ export type TRelayThreadHeatEdge = { a: string; b: string }
/** Minimum feed-filtered notes in a thread to appear as a bubble. */ /** Minimum feed-filtered notes in a thread to appear as a bubble. */
export const RELAY_THREAD_HEAT_MIN_INTERACTIONS = 5 export const RELAY_THREAD_HEAT_MIN_INTERACTIONS = 5
function collapseSnippet(content: string, maxLen = 160): string { export function collapseRelayThreadHeatSnippet(content: string, maxLen = 160): string {
const t = content.replace(/\s+/g, ' ').trim().slice(0, maxLen) const t = content.replace(/\s+/g, ' ').trim().slice(0, maxLen)
return t || '…' return t || '…'
} }
@ -107,8 +107,8 @@ export function buildRelayThreadHeatBubbles(
) )
const kind1TopLevel = kind1Or11.find((e) => e.kind === kinds.ShortTextNote && !isReplyNoteEvent(e)) const kind1TopLevel = kind1Or11.find((e) => e.kind === kinds.ShortTextNote && !isReplyNoteEvent(e))
if (kind1TopLevel) return kind1TopLevel if (kind1TopLevel) return kind1TopLevel
const sorted = [...kind1Or11].sort((a, b) => a.created_at - b.created_at) // OP may be outside the heat window or not in this merge; never use an early reply as OP text.
return sorted[0] return undefined
})() })()
const snippetSource = opForSnippet?.content?.trim() ?? '' const snippetSource = opForSnippet?.content?.trim() ?? ''
@ -118,7 +118,7 @@ export function buildRelayThreadHeatBubbles(
postCount, postCount,
uniqueAuthors, uniqueAuthors,
followAuthorsInThread, followAuthorsInThread,
snippet: collapseSnippet(snippetSource), snippet: collapseRelayThreadHeatSnippet(snippetSource),
lastActivity, lastActivity,
rootEvent rootEvent
}) })

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

@ -16,6 +16,7 @@ import { orderHeatBubblesByKeywordProximity } from '@/lib/relay-thread-heat-keyw
import { import {
buildRelayThreadHeatBubbles, buildRelayThreadHeatBubbles,
buildRelayThreadHeatEdges, buildRelayThreadHeatEdges,
collapseRelayThreadHeatSnippet,
RELAY_THREAD_HEAT_MIN_INTERACTIONS, RELAY_THREAD_HEAT_MIN_INTERACTIONS,
type TRelayThreadHeatBubble, type TRelayThreadHeatBubble,
type TRelayThreadHeatEdge type TRelayThreadHeatEdge
@ -46,6 +47,8 @@ const HEAT_KINDS = [kinds.ShortTextNote, ExtendedKind.DISCUSSION] as const
const ARCHIVE_SCAN_TIMEOUT_MS = 22_000 const ARCHIVE_SCAN_TIMEOUT_MS = 22_000
const RELAY_FETCH_TIMEOUT_MS = 28_000 const RELAY_FETCH_TIMEOUT_MS = 28_000
/** Load thread roots that sit outside the heat time window so hover text is the OP, not a reply. */
const ROOT_SNIPPET_FETCH_TIMEOUT_MS = 12_000
const TOMBSTONES_TIMEOUT_MS = 8_000 const TOMBSTONES_TIMEOUT_MS = 8_000
function raceWithTimeout<T>(promise: Promise<T>, ms: number, fallback: T, label: string): Promise<T> { function raceWithTimeout<T>(promise: Promise<T>, ms: number, fallback: T, label: string): Promise<T> {
@ -192,9 +195,58 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props)
eventPassesNoteListKindPicker(e, showKinds, showKind1OPs, showKind1Replies, showKind1111) eventPassesNoteListKindPicker(e, showKinds, showKind1OPs, showKind1Replies, showKind1111)
) )
const ranked = buildRelayThreadHeatBubbles(feedNotes, followSet, windowStart) const ranked = buildRelayThreadHeatBubbles(feedNotes, followSet, windowStart)
const bubbles = ranked let bubbles = ranked
.filter((b) => b.postCount >= RELAY_THREAD_HEAT_MIN_INTERACTIONS) .filter((b) => b.postCount >= RELAY_THREAD_HEAT_MIN_INTERACTIONS)
.slice(0, MAX_BUBBLES) .slice(0, MAX_BUBBLES)
const missingRootIds = [
...new Set(
bubbles
.filter((b) => !b.rootEvent)
.map((b) => b.rootId.trim().toLowerCase())
.filter((id) => /^[0-9a-f]{64}$/.test(id))
)
]
if (missingRootIds.length > 0) {
const rootById = new Map<string, Event>()
const archived = await indexedDb.getArchivedEventsByIds(missingRootIds)
for (const ev of archived) {
if (!verifyEvent(ev)) continue
if (ev.kind !== kinds.ShortTextNote && ev.kind !== ExtendedKind.DISCUSSION) continue
rootById.set(ev.id.toLowerCase(), ev)
}
const stillMissing = missingRootIds.filter((id) => !rootById.has(id))
if (stillMissing.length > 0 && relayUrls.length > 0) {
const fetched = await raceWithTimeout(
client.fetchEvents(
relayUrls,
{ ids: stillMissing, kinds: [...HEAT_KINDS] },
{ eoseTimeout: 6000, globalTimeout: ROOT_SNIPPET_FETCH_TIMEOUT_MS }
),
ROOT_SNIPPET_FETCH_TIMEOUT_MS,
[] as Event[],
'heat-map-root-snippet'
)
for (const ev of fetched) {
if (!verifyEvent(ev)) continue
if (ev.kind !== kinds.ShortTextNote && ev.kind !== ExtendedKind.DISCUSSION) continue
rootById.set(ev.id.toLowerCase(), ev)
}
}
if (rootById.size > 0) {
bubbles = bubbles.map((b) => {
if (b.rootEvent) return b
const ev = rootById.get(b.rootId.toLowerCase())
if (!ev) return b
return {
...b,
rootEvent: ev,
snippet: collapseRelayThreadHeatSnippet(ev.content?.trim() ?? '')
}
})
}
}
const roots = new Set(bubbles.map((b) => b.rootId)) const roots = new Set(bubbles.map((b) => b.rootId))
const edges = buildRelayThreadHeatEdges(feedNotes, roots) const edges = buildRelayThreadHeatEdges(feedNotes, roots)
logger.info('[RelayThreadHeatMap] merge finished', { logger.info('[RelayThreadHeatMap] merge finished', {

Loading…
Cancel
Save