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 @@ @@ -1,12 +1,12 @@
{
"name": "imwald",
"version": "23.6.0",
"version": "23.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "imwald",
"version": "23.6.0",
"version": "23.7.0",
"license": "MIT",
"dependencies": {
"@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"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",
"private": true,
"type": "module",

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

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

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

@ -16,6 +16,7 @@ import { orderHeatBubblesByKeywordProximity } from '@/lib/relay-thread-heat-keyw @@ -16,6 +16,7 @@ import { orderHeatBubblesByKeywordProximity } from '@/lib/relay-thread-heat-keyw
import {
buildRelayThreadHeatBubbles,
buildRelayThreadHeatEdges,
collapseRelayThreadHeatSnippet,
RELAY_THREAD_HEAT_MIN_INTERACTIONS,
type TRelayThreadHeatBubble,
type TRelayThreadHeatEdge
@ -46,6 +47,8 @@ const HEAT_KINDS = [kinds.ShortTextNote, ExtendedKind.DISCUSSION] as const @@ -46,6 +47,8 @@ const HEAT_KINDS = [kinds.ShortTextNote, ExtendedKind.DISCUSSION] as const
const ARCHIVE_SCAN_TIMEOUT_MS = 22_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
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) @@ -192,9 +195,58 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props)
eventPassesNoteListKindPicker(e, showKinds, showKind1OPs, showKind1Replies, showKind1111)
)
const ranked = buildRelayThreadHeatBubbles(feedNotes, followSet, windowStart)
const bubbles = ranked
let bubbles = ranked
.filter((b) => b.postCount >= RELAY_THREAD_HEAT_MIN_INTERACTIONS)
.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 edges = buildRelayThreadHeatEdges(feedNotes, roots)
logger.info('[RelayThreadHeatMap] merge finished', {

Loading…
Cancel
Save