diff --git a/src/constants.ts b/src/constants.ts
index 8b79c8b5..7cedae53 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1005,6 +1005,7 @@ export const FAUX_SPELL_ORDER = [
'followPacks',
'media',
'interests',
+ 'nostrSpecs',
'bookmarks',
'calendar'
] as const
diff --git a/src/i18n/locales/cs.ts b/src/i18n/locales/cs.ts
index b3153bb7..91d0b2c5 100644
--- a/src/i18n/locales/cs.ts
+++ b/src/i18n/locales/cs.ts
@@ -9,17 +9,6 @@ export default {
Home: 'Home',
Feed: 'Feed',
'Favorite Relays': 'Favorite Relays',
- 'Relay pulse': 'Relay pulse',
- 'Relay pulse empty': 'Quiet on your relays in the last hour.',
- 'Relay pulse follows': 'Following ({{count}})',
- 'Relay pulse others': 'Others ({{count}})',
- 'Relay pulse updated': 'Updated {{relative}}',
- 'Relay pulse active npubs': 'Active npubs',
- 'Relay pulse active npubs hint':
- 'Kind 0 profiles for pubkeys seen on your favorite relays in the last hour (same sample as Relay pulse).',
- 'Relay pulse drawer following': 'Following',
- 'Relay pulse drawer others': 'Others',
- 'Relay pulse drawer no profiles': 'No kind 0 profiles loaded for this sample yet.',
'See the newest notes from your follows': 'See the newest notes from your follows',
'All favorite relays': 'All favorite relays',
'Pinned note': 'Pinned note',
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index 1658f5ab..0028138a 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -9,18 +9,6 @@ export default {
Home: 'Startseite',
Feed: 'Feed',
'Favorite Relays': 'Lieblings-Relays',
- 'Relay pulse': 'Relay-Puls',
- 'Relay pulse empty': 'In der letzten Stunde war es ruhig auf deinen Relays.',
- 'Relay pulse follows': 'Folge ich ({{count}})',
- 'Relay pulse others': 'Andere ({{count}})',
- 'Relay pulse updated': 'Aktualisiert {{relative}}',
- 'Relay pulse active npubs': 'Aktive npubs',
- 'Relay pulse active npubs hint':
- 'Kind-0-Profile für npubs, die in der letzten Stunde auf deinen Lieblingsrelais auftauchten (gleiche Stichprobe wie Relay-Puls).',
- 'Relay pulse drawer following': 'Folge ich',
- 'Relay pulse drawer others': 'Andere',
- 'Relay pulse drawer no profiles':
- 'Für diese Stichprobe wurden noch keine Kind-0-Profile geladen.',
'See the newest notes from your follows': 'Neueste Notizen von deinen Abos anzeigen',
'All favorite relays': 'Alle Lieblingsrelais',
'Pinned note': 'Angehefteter Beitrag',
@@ -2561,6 +2549,9 @@ export default {
'Website where LLM was accessed (optional)': 'Website where LLM was accessed (optional)',
'Wiki Article (AsciiDoc)': 'Wiki Article (AsciiDoc)',
'Nostr Specification': 'Nostr Specification',
+ 'Nostr specs': 'Nostr-Spezifikationen',
+ 'Nostr spec affected kinds': 'Kinds {{kinds}}',
+ 'Download as Markdown file': 'Als Markdown-Datei herunterladen',
'You can only delete your own notes': 'You can only delete your own notes',
'You must be logged in to create a thread': 'You must be logged in to create a thread',
'You need to add at least one media server in order to upload media files.':
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 65345e9c..838b0dfb 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -7,17 +7,6 @@ export default {
Home: 'Home',
Feed: 'Feed',
'Favorite Relays': 'Favorite Relays',
- 'Relay pulse': 'Relay pulse',
- 'Relay pulse empty': 'Quiet on your relays in the last hour.',
- 'Relay pulse follows': 'Following ({{count}})',
- 'Relay pulse others': 'Others ({{count}})',
- 'Relay pulse updated': 'Updated {{relative}}',
- 'Relay pulse active npubs': 'Active npubs',
- 'Relay pulse active npubs hint':
- 'Kind 0 profiles for pubkeys seen on your favorite relays in the last hour (same sample as Relay pulse).',
- 'Relay pulse drawer following': 'Following',
- 'Relay pulse drawer others': 'Others',
- 'Relay pulse drawer no profiles': 'No kind 0 profiles loaded for this sample yet.',
'See the newest notes from your follows': 'See the newest notes from your follows',
'All favorite relays': 'All favorite relays',
'Pinned note': 'Pinned note',
@@ -2530,6 +2519,9 @@ export default {
'Website where LLM was accessed (optional)': 'Website where LLM was accessed (optional)',
'Wiki Article (AsciiDoc)': 'Wiki Article (AsciiDoc)',
'Nostr Specification': 'Nostr Specification',
+ 'Nostr specs': 'Nostr specs',
+ 'Nostr spec affected kinds': 'Kinds {{kinds}}',
+ 'Download as Markdown file': 'Download as Markdown file',
'You can only delete your own notes': 'You can only delete your own notes',
'You must be logged in to create a thread': 'You must be logged in to create a thread',
'You need to add at least one media server in order to upload media files.':
diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts
index 0a2faf85..44c59500 100644
--- a/src/i18n/locales/es.ts
+++ b/src/i18n/locales/es.ts
@@ -9,17 +9,6 @@ export default {
Home: 'Inicio',
Feed: 'Feed',
'Favorite Relays': 'Relés favoritos',
- 'Relay pulse': 'Relay pulse',
- 'Relay pulse empty': 'Quiet on your relays in the last hour.',
- 'Relay pulse follows': 'Following ({{count}})',
- 'Relay pulse others': 'Others ({{count}})',
- 'Relay pulse updated': 'Updated {{relative}}',
- 'Relay pulse active npubs': 'Active npubs',
- 'Relay pulse active npubs hint':
- 'Kind 0 profiles for pubkeys seen on your favorite relays in the last hour (same sample as Relay pulse).',
- 'Relay pulse drawer following': 'Following',
- 'Relay pulse drawer others': 'Others',
- 'Relay pulse drawer no profiles': 'No kind 0 profiles loaded for this sample yet.',
'See the newest notes from your follows': 'See the newest notes from your follows',
'All favorite relays': 'Todos los relés favoritos',
'Pinned note': 'Pinned note',
diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts
index 213727ae..1c50b1de 100644
--- a/src/i18n/locales/fr.ts
+++ b/src/i18n/locales/fr.ts
@@ -9,17 +9,6 @@ export default {
Home: 'Accueil',
Feed: 'Feed',
'Favorite Relays': 'Relais favoris',
- 'Relay pulse': 'Relay pulse',
- 'Relay pulse empty': 'Quiet on your relays in the last hour.',
- 'Relay pulse follows': 'Following ({{count}})',
- 'Relay pulse others': 'Others ({{count}})',
- 'Relay pulse updated': 'Updated {{relative}}',
- 'Relay pulse active npubs': 'Active npubs',
- 'Relay pulse active npubs hint':
- 'Kind 0 profiles for pubkeys seen on your favorite relays in the last hour (same sample as Relay pulse).',
- 'Relay pulse drawer following': 'Following',
- 'Relay pulse drawer others': 'Others',
- 'Relay pulse drawer no profiles': 'No kind 0 profiles loaded for this sample yet.',
'See the newest notes from your follows': 'See the newest notes from your follows',
'All favorite relays': 'Tous les relais favoris',
'Pinned note': 'Pinned note',
diff --git a/src/i18n/locales/nl.ts b/src/i18n/locales/nl.ts
index a1c786b8..37e4abaa 100644
--- a/src/i18n/locales/nl.ts
+++ b/src/i18n/locales/nl.ts
@@ -9,17 +9,6 @@ export default {
Home: 'Home',
Feed: 'Feed',
'Favorite Relays': 'Favorite Relays',
- 'Relay pulse': 'Relay pulse',
- 'Relay pulse empty': 'Quiet on your relays in the last hour.',
- 'Relay pulse follows': 'Following ({{count}})',
- 'Relay pulse others': 'Others ({{count}})',
- 'Relay pulse updated': 'Updated {{relative}}',
- 'Relay pulse active npubs': 'Active npubs',
- 'Relay pulse active npubs hint':
- 'Kind 0 profiles for pubkeys seen on your favorite relays in the last hour (same sample as Relay pulse).',
- 'Relay pulse drawer following': 'Following',
- 'Relay pulse drawer others': 'Others',
- 'Relay pulse drawer no profiles': 'No kind 0 profiles loaded for this sample yet.',
'See the newest notes from your follows': 'See the newest notes from your follows',
'All favorite relays': 'All favorite relays',
'Pinned note': 'Pinned note',
diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts
index 45ddfd3f..6552a664 100644
--- a/src/i18n/locales/pl.ts
+++ b/src/i18n/locales/pl.ts
@@ -9,17 +9,6 @@ export default {
Home: 'Strona Główna',
Feed: 'Feed',
'Favorite Relays': 'Ulubione transmitery',
- 'Relay pulse': 'Relay pulse',
- 'Relay pulse empty': 'Quiet on your relays in the last hour.',
- 'Relay pulse follows': 'Following ({{count}})',
- 'Relay pulse others': 'Others ({{count}})',
- 'Relay pulse updated': 'Updated {{relative}}',
- 'Relay pulse active npubs': 'Active npubs',
- 'Relay pulse active npubs hint':
- 'Kind 0 profiles for pubkeys seen on your favorite relays in the last hour (same sample as Relay pulse).',
- 'Relay pulse drawer following': 'Following',
- 'Relay pulse drawer others': 'Others',
- 'Relay pulse drawer no profiles': 'No kind 0 profiles loaded for this sample yet.',
'See the newest notes from your follows': 'See the newest notes from your follows',
'All favorite relays': 'Wszystkie ulubione transmitery',
'Pinned note': 'Pinned note',
diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts
index 8db824bf..d883fb83 100644
--- a/src/i18n/locales/ru.ts
+++ b/src/i18n/locales/ru.ts
@@ -9,17 +9,6 @@ export default {
Home: 'Главная',
Feed: 'Feed',
'Favorite Relays': 'Избранные ретрансляторы',
- 'Relay pulse': 'Relay pulse',
- 'Relay pulse empty': 'Quiet on your relays in the last hour.',
- 'Relay pulse follows': 'Following ({{count}})',
- 'Relay pulse others': 'Others ({{count}})',
- 'Relay pulse updated': 'Updated {{relative}}',
- 'Relay pulse active npubs': 'Active npubs',
- 'Relay pulse active npubs hint':
- 'Kind 0 profiles for pubkeys seen on your favorite relays in the last hour (same sample as Relay pulse).',
- 'Relay pulse drawer following': 'Following',
- 'Relay pulse drawer others': 'Others',
- 'Relay pulse drawer no profiles': 'No kind 0 profiles loaded for this sample yet.',
'See the newest notes from your follows': 'See the newest notes from your follows',
'All favorite relays': 'Все избранные ретрансляторы',
'Pinned note': 'Pinned note',
diff --git a/src/i18n/locales/tr.ts b/src/i18n/locales/tr.ts
index 613158de..da902559 100644
--- a/src/i18n/locales/tr.ts
+++ b/src/i18n/locales/tr.ts
@@ -9,17 +9,6 @@ export default {
Home: 'Home',
Feed: 'Feed',
'Favorite Relays': 'Favorite Relays',
- 'Relay pulse': 'Relay pulse',
- 'Relay pulse empty': 'Quiet on your relays in the last hour.',
- 'Relay pulse follows': 'Following ({{count}})',
- 'Relay pulse others': 'Others ({{count}})',
- 'Relay pulse updated': 'Updated {{relative}}',
- 'Relay pulse active npubs': 'Active npubs',
- 'Relay pulse active npubs hint':
- 'Kind 0 profiles for pubkeys seen on your favorite relays in the last hour (same sample as Relay pulse).',
- 'Relay pulse drawer following': 'Following',
- 'Relay pulse drawer others': 'Others',
- 'Relay pulse drawer no profiles': 'No kind 0 profiles loaded for this sample yet.',
'See the newest notes from your follows': 'See the newest notes from your follows',
'All favorite relays': 'All favorite relays',
'Pinned note': 'Pinned note',
diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts
index ace98b14..598d9fb1 100644
--- a/src/i18n/locales/zh.ts
+++ b/src/i18n/locales/zh.ts
@@ -9,17 +9,6 @@ export default {
Home: '主页',
Feed: 'Feed',
'Favorite Relays': '收藏的服务器',
- 'Relay pulse': 'Relay pulse',
- 'Relay pulse empty': 'Quiet on your relays in the last hour.',
- 'Relay pulse follows': 'Following ({{count}})',
- 'Relay pulse others': 'Others ({{count}})',
- 'Relay pulse updated': 'Updated {{relative}}',
- 'Relay pulse active npubs': 'Active npubs',
- 'Relay pulse active npubs hint':
- 'Kind 0 profiles for pubkeys seen on your favorite relays in the last hour (same sample as Relay pulse).',
- 'Relay pulse drawer following': 'Following',
- 'Relay pulse drawer others': 'Others',
- 'Relay pulse drawer no profiles': 'No kind 0 profiles loaded for this sample yet.',
'See the newest notes from your follows': 'See the newest notes from your follows',
'All favorite relays': '所有收藏服务器',
'Pinned note': 'Pinned note',
diff --git a/src/lib/download-event-markdown.ts b/src/lib/download-event-markdown.ts
new file mode 100644
index 00000000..fdfda455
--- /dev/null
+++ b/src/lib/download-event-markdown.ts
@@ -0,0 +1,24 @@
+import type { Event } from 'nostr-tools'
+
+function markdownFilename(title: string | undefined): string {
+ const base = (title?.trim() || 'document')
+ .replace(/[<>:"/\\|?*\u0000-\u001f]/g, '')
+ .replace(/\s+/g, ' ')
+ .trim()
+ .slice(0, 120)
+ return `${base || 'document'}.md`
+}
+
+/** Trigger a browser download of the event body as a `.md` file. */
+export function downloadEventAsMarkdownFile(event: Event, title?: string): void {
+ const filename = markdownFilename(title)
+ const blob = new Blob([event.content], { type: 'text/markdown;charset=utf-8' })
+ const url = URL.createObjectURL(blob)
+ const anchor = document.createElement('a')
+ anchor.href = url
+ anchor.download = filename
+ document.body.appendChild(anchor)
+ anchor.click()
+ document.body.removeChild(anchor)
+ URL.revokeObjectURL(url)
+}
diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts
index 296b1567..61e0d577 100644
--- a/src/lib/event-metadata.ts
+++ b/src/lib/event-metadata.ts
@@ -130,7 +130,7 @@ export function getRelayListFromEvent(
/**
* Read-side `r` tags from a relay list event (e.g. kind 10012) without {@link FAST_READ_RELAY_URLS} fallback
- * when the list is empty or oversized — for strict “viewer-owned” REQ stacks (relay pulse).
+ * when the list is empty or oversized — for strict viewer-owned REQ stacks.
*/
export function getRelayListReadFromEventNoFastFallback(
event: Event | null | undefined,
diff --git a/src/lib/home-feed-relays.ts b/src/lib/home-feed-relays.ts
index 69d8a348..7edd8537 100644
--- a/src/lib/home-feed-relays.ts
+++ b/src/lib/home-feed-relays.ts
@@ -1,10 +1,6 @@
-import { MAX_REQ_RELAY_URLS } from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
-import { getHttpRelayListFromEvent, getRelayListReadFromEventNoFastFallback } from '@/lib/event-metadata'
import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays'
import { stripNostrLandAggrFromRelayUrls } from '@/lib/nostr-land-relay-eligibility'
-import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
-import type { Event } from 'nostr-tools'
export { stripNostrLandAggrFromRelayUrls }
@@ -51,62 +47,3 @@ export function buildAllFavoritesFeedRelayUrls(
)
)
}
-
-/**
- * Relay pulse (sidebar active authors): only the viewer’s own stack — favorites (+ relay sets),
- * NIP-65 read, kind 10012 cache read, and HTTP index reads — never the global fast-read layer.
- */
-export function buildRelayPulseQueryRelayUrls(options: {
- viewerPubkey: string | null | undefined
- favoriteRelayUrls: string[]
- blockedRelays: string[]
- relayList: { read?: string[]; httpRead?: string[] } | null | undefined
- cacheRelayListEvent: Event | null | undefined
- httpRelayListEvent: Event | null | undefined
-}): string[] {
- const {
- viewerPubkey,
- favoriteRelayUrls,
- blockedRelays,
- relayList,
- cacheRelayListEvent,
- httpRelayListEvent
- } = options
-
- const useGlobalFavoriteDefaults = viewerUsesGlobalRelayDefaults({
- viewerPubkey,
- favoriteRelayUrls,
- relayList
- })
- const primaryRelays = getFavoritesFeedRelayUrls(favoriteRelayUrls, blockedRelays, useGlobalFavoriteDefaults)
- const inboxRelayUrls = relayList?.read?.length ? relayList.read : []
-
- const cacheRelayUrls: string[] = []
- if (cacheRelayListEvent) {
- cacheRelayUrls.push(...getRelayListReadFromEventNoFastFallback(cacheRelayListEvent, blockedRelays))
- }
-
- const httpRelayUrls: string[] = [...(relayList?.httpRead ?? [])]
- if (httpRelayListEvent) {
- httpRelayUrls.push(...getHttpRelayListFromEvent(httpRelayListEvent, blockedRelays).httpRead)
- }
-
- return stripNostrLandAggrFromRelayUrls(
- feedRelayPolicyUrls(
- [
- { source: 'favorites', urls: primaryRelays },
- { source: 'viewer-read', urls: inboxRelayUrls },
- { source: 'cache', urls: cacheRelayUrls },
- { source: 'http-index', urls: httpRelayUrls }
- ],
- {
- operation: 'read',
- blockedRelays,
- nostrLandAggr: 'never',
- applySocialKindBlockedFilter: false,
- allowThirdPartyLocalRelays: true,
- maxRelays: MAX_REQ_RELAY_URLS
- }
- )
- )
-}
diff --git a/src/lib/nostr-spec-affected-kinds.test.ts b/src/lib/nostr-spec-affected-kinds.test.ts
index 8cabcc87..4b0bd40b 100644
--- a/src/lib/nostr-spec-affected-kinds.test.ts
+++ b/src/lib/nostr-spec-affected-kinds.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { parseNostrSpecAffectedKinds } from './nostr-spec-affected-kinds'
+import { parseNostrSpecAffectedKinds, parseNostrSpecAffectedKindsFromEvent } from './nostr-spec-affected-kinds'
describe('parseNostrSpecAffectedKinds', () => {
it('parses one kind per row and dedupes', () => {
@@ -22,3 +22,18 @@ describe('parseNostrSpecAffectedKinds', () => {
).toEqual([])
})
})
+
+describe('parseNostrSpecAffectedKindsFromEvent', () => {
+ it('reads numeric k tags from the event', () => {
+ expect(
+ parseNostrSpecAffectedKindsFromEvent({
+ tags: [
+ ['d', 'nip-01'],
+ ['k', '1'],
+ ['k', '7'],
+ ['k', '1']
+ ]
+ })
+ ).toEqual([1, 7])
+ })
+})
diff --git a/src/lib/nostr-spec-affected-kinds.ts b/src/lib/nostr-spec-affected-kinds.ts
index 2461d118..e5c6018d 100644
--- a/src/lib/nostr-spec-affected-kinds.ts
+++ b/src/lib/nostr-spec-affected-kinds.ts
@@ -18,3 +18,17 @@ export function parseNostrSpecAffectedKinds(rows: NostrSpecAffectedKindRow[]): n
}
return out
}
+
+/** Kind numbers from `k` tags on a published Nostr specification (30817). */
+export function parseNostrSpecAffectedKindsFromEvent(event: { tags: string[][] }): number[] {
+ const seen = new Set
()
+ const out: number[] = []
+ for (const tag of event.tags) {
+ if (tag[0] !== 'k' || !tag[1]) continue
+ const n = Number.parseInt(tag[1], 10)
+ if (!Number.isInteger(n) || n < 0 || seen.has(n)) continue
+ seen.add(n)
+ out.push(n)
+ }
+ return out.sort((a, b) => a - b)
+}
diff --git a/src/lib/relay-pulse-active-npubs-cache.ts b/src/lib/relay-pulse-active-npubs-cache.ts
deleted file mode 100644
index 352489d3..00000000
--- a/src/lib/relay-pulse-active-npubs-cache.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import logger from '@/lib/logger'
-
-/** One row per browser; overwritten whenever a new active-npub list is fetched for the same relay + viewer scope. */
-export type RelayPulseActiveNpubsCacheRow = {
- relayKey: string
- viewerPubkey: string | null
- orderedPubkeys: string[]
- lastFetchedAtMs: number
-}
-
-const STORAGE_KEY = 'jumble.relayPulse.activeNpubs.v1'
-
-export function readRelayPulseActiveNpubsCache(
- relayKey: string,
- viewerPubkey: string | null
-): Pick | null {
- try {
- const raw = localStorage.getItem(STORAGE_KEY)
- if (!raw) return null
- const data = JSON.parse(raw) as unknown
- if (!data || typeof data !== 'object') return null
- const o = data as Record
- if (o.relayKey !== relayKey || o.viewerPubkey !== viewerPubkey) return null
- if (!Array.isArray(o.orderedPubkeys) || typeof o.lastFetchedAtMs !== 'number') return null
- const orderedPubkeys = o.orderedPubkeys.filter((x): x is string => typeof x === 'string')
- return { orderedPubkeys, lastFetchedAtMs: o.lastFetchedAtMs }
- } catch {
- return null
- }
-}
-
-export function writeRelayPulseActiveNpubsCache(row: RelayPulseActiveNpubsCacheRow): void {
- try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(row))
- } catch (e) {
- logger.debug('[RelayPulseActiveNpubsCache] write failed', { error: e })
- }
-}
diff --git a/src/lib/relay-pulse-nip05.ts b/src/lib/relay-pulse-nip05.ts
deleted file mode 100644
index 7235e894..00000000
--- a/src/lib/relay-pulse-nip05.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { Event } from 'nostr-tools'
-
-function addNip05(set: Set, raw: unknown) {
- if (typeof raw !== 'string') return
- const t = raw.trim()
- if (t) set.add(t)
-}
-
-/**
- * All NIP-05 identifiers from kind 0: every `nip05` tag plus JSON `nip05` (string or string array).
- * Deduplicated, order not preserved.
- */
-export function collectAggregatedNip05sFromKind0(event: Event): string[] {
- const set = new Set()
- for (const tag of event.tags) {
- if (tag[0] === 'nip05' && tag[1]) addNip05(set, tag[1])
- }
- try {
- const obj = JSON.parse(event.content || '{}') as Record
- const j = obj.nip05
- if (typeof j === 'string') addNip05(set, j)
- else if (Array.isArray(j)) {
- for (const x of j) addNip05(set, x)
- }
- } catch {
- // ignore invalid JSON
- }
- return [...set]
-}
diff --git a/src/pages/primary/NoteListPage/RelaysFeed.tsx b/src/pages/primary/NoteListPage/RelaysFeed.tsx
index 15a2ddad..0d1c9312 100644
--- a/src/pages/primary/NoteListPage/RelaysFeed.tsx
+++ b/src/pages/primary/NoteListPage/RelaysFeed.tsx
@@ -30,6 +30,17 @@ const RelaysFeed = forwardRef<
.join('|'),
[relayUrls]
)
+ const replyRelayUrlsKey = useMemo(
+ () =>
+ [...replyRelayUrls]
+ .map((u) => normalizeUrl(u) || u)
+ .filter(Boolean)
+ .sort()
+ .join('|'),
+ [replyRelayUrls]
+ )
+ const homeFeedSeenOnAllowlistOp = useMemo(() => relayUrls, [relayUrlsKey])
+ const homeFeedSeenOnAllowlistReplies = useMemo(() => replyRelayUrls, [replyRelayUrlsKey])
useEffect(() => {
if (relayUrls.length === 0) {
@@ -85,7 +96,7 @@ const RelaysFeed = forwardRef<
}
}
]
- }, [canRenderFeed, relayUrls, defaultKinds])
+ }, [canRenderFeed, relayUrlsKey, relayUrls, defaultKinds])
const repliesSubRequests = useMemo(() => {
if (!canRenderFeed) return []
return [
@@ -96,7 +107,7 @@ const RelaysFeed = forwardRef<
}
}
]
- }, [canRenderFeed, replyRelayUrls, relayUrls, defaultKinds])
+ }, [canRenderFeed, replyRelayUrlsKey, replyRelayUrls, relayUrlsKey, relayUrls, defaultKinds])
if (!canRenderFeed) {
return null
@@ -117,8 +128,8 @@ const RelaysFeed = forwardRef<
widenMainGalleryRelays={false}
feedSubscriptionKey="home-all-favorites"
feedTimelineScopeKey="all-favorites"
- homeFeedSeenOnAllowlistOp={relayUrls}
- homeFeedSeenOnAllowlistReplies={replyRelayUrls}
+ homeFeedSeenOnAllowlistOp={homeFeedSeenOnAllowlistOp}
+ homeFeedSeenOnAllowlistReplies={homeFeedSeenOnAllowlistReplies}
showFeedClientFilter
hostPrimaryPageName="feed"
/>
diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx
index 976792ee..bf636b7c 100644
--- a/src/pages/primary/NoteListPage/index.tsx
+++ b/src/pages/primary/NoteListPage/index.tsx
@@ -5,6 +5,7 @@ import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFeed } from '@/providers/feed-context'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
+import { normalizeUrl } from '@/lib/url'
import type { TNoteListRef } from '@/components/NoteList'
import { TPageRef } from '@/types'
import { Calendar, Compass, Flame } from 'lucide-react'
@@ -13,11 +14,11 @@ import React, {
useCallback,
useEffect,
useImperativeHandle,
+ useMemo,
useRef,
useState
} from 'react'
import { useTranslation } from 'react-i18next'
-import { FavoriteRelaysActiveStripMobileBar } from '@/components/FavoriteRelaysActiveStrip'
import Logo from '@/assets/Logo'
import RelaysFeed from './RelaysFeed'
import { usePrimaryPage } from '@/contexts/primary-page-context'
@@ -28,6 +29,15 @@ const NoteListPage = forwardRef((_, ref) => {
const layoutRef = useRef(null)
const feedRef = useRef(null)
const { relayUrls } = useFeed()
+ const relayUrlsKey = useMemo(
+ () =>
+ [...relayUrls]
+ .map((u) => normalizeUrl(u) || u)
+ .filter(Boolean)
+ .sort()
+ .join('|'),
+ [relayUrls]
+ )
const { isSmallScreen } = useScreenSize()
const [homeSubHeader, setHomeSubHeader] = useState(null)
@@ -52,19 +62,18 @@ const NoteListPage = forwardRef((_, ref) => {
// The feed stays mounted and maintains scroll position at all times
useEffect(() => {
- if (relayUrls.length) {
- addRelayUrls(relayUrls)
- return () => {
- removeRelayUrls(relayUrls)
- }
+ const urls = relayUrlsKey.split('|').filter(Boolean)
+ if (!urls.length) return
+ addRelayUrls(urls)
+ return () => {
+ removeRelayUrls(urls)
}
- }, [relayUrls])
+ }, [relayUrlsKey, addRelayUrls, removeRelayUrls])
const feedPageTitle = t('Favorite Relays')
const subHeader = (
<>
- {isSmallScreen ? : null}
{feedPageTitle}
diff --git a/src/pages/primary/SpellsPage/fauxSpellConfig.ts b/src/pages/primary/SpellsPage/fauxSpellConfig.ts
index 79820cd5..3e53c948 100644
--- a/src/pages/primary/SpellsPage/fauxSpellConfig.ts
+++ b/src/pages/primary/SpellsPage/fauxSpellConfig.ts
@@ -12,6 +12,7 @@ import {
CalendarDays,
Flame,
Map as MapIcon,
+ FileText,
Gift,
Hash,
Image as ImageIcon,
@@ -73,6 +74,8 @@ export function fauxSpellLabelKey(name: FauxSpellName): string {
return 'Media'
case 'interests':
return 'Interests'
+ case 'nostrSpecs':
+ return 'Nostr specs'
case 'bookmarks':
return 'Bookmarks'
case 'calendar':
@@ -91,6 +94,7 @@ export const FAUX_SPELL_ICON: Record = {
followPacks: Gift,
media: ImageIcon,
interests: Hash,
+ nostrSpecs: FileText,
bookmarks: Bookmark,
calendar: CalendarDays
}
diff --git a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
index 6ce5764e..39a09e6b 100644
--- a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
+++ b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
@@ -244,6 +244,13 @@ export function buildCalendarSpellFilter(): Filter {
}
}
+export function buildNostrSpecsSpellFilter(): Filter {
+ return {
+ kinds: [ExtendedKind.NOSTR_SPECIFICATION],
+ limit: FAUX_SPELL_EVENT_LIMIT
+ }
+}
+
function pluralizeTopic(topic: string): string {
if (!topic) return topic
if (topic.endsWith('y') && topic.length > 1 && !/[aeiou]y$/i.test(topic)) {
diff --git a/src/pages/primary/SpellsPage/useSpellsPageFeed.ts b/src/pages/primary/SpellsPage/useSpellsPageFeed.ts
index 72636f1c..7c1f6f2b 100644
--- a/src/pages/primary/SpellsPage/useSpellsPageFeed.ts
+++ b/src/pages/primary/SpellsPage/useSpellsPageFeed.ts
@@ -30,6 +30,7 @@ import {
buildDiscussionFilter,
buildInterestsSubRequests,
buildMediaSpellFilter,
+ buildNostrSpecsSpellFilter,
buildNotificationsFollowedThreadSubRequests,
buildNotificationsSpellSubRequests,
buildWebBookmarksSpellSubRequests,
@@ -390,6 +391,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
selectedFauxSpell === 'calendar' ||
selectedFauxSpell === 'followPacks' ||
selectedFauxSpell === 'media' ||
+ selectedFauxSpell === 'nostrSpecs' ||
selectedFauxSpell === 'bookmarks' ||
selectedFauxSpell === 'interests'
const feedUrls = ensureFauxSpellRelayStackTouchesFastRead(
@@ -426,6 +428,10 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
if (!feedUrls.length) return []
return [{ urls: feedUrls, filter: buildCalendarSpellFilter() }]
}
+ if (selectedFauxSpell === 'nostrSpecs') {
+ if (!feedUrls.length) return []
+ return [{ urls: feedUrls, filter: buildNostrSpecsSpellFilter() }]
+ }
if (selectedFauxSpell === 'interests') {
if (!pubkey || !interestListEvent) return []
const topics = interestListEvent.tags.filter((tag) => tag[0] === 't' && tag[1]).map((tag) => tag[1]!)
@@ -547,6 +553,9 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
if (selectedFauxSpell === 'calendar') {
return [ExtendedKind.CALENDAR_EVENT_DATE, ExtendedKind.CALENDAR_EVENT_TIME]
}
+ if (selectedFauxSpell === 'nostrSpecs') {
+ return [ExtendedKind.NOSTR_SPECIFICATION]
+ }
if (selectedFauxSpell === 'interests') {
return [...DEFAULT_FEED_SHOW_KINDS]
}
diff --git a/src/providers/FavoriteRelaysActivityProvider.tsx b/src/providers/FavoriteRelaysActivityProvider.tsx
deleted file mode 100644
index 18526323..00000000
--- a/src/providers/FavoriteRelaysActivityProvider.tsx
+++ /dev/null
@@ -1,447 +0,0 @@
-import storage from '@/services/local-storage.service'
-import logger from '@/lib/logger'
-import { ExtendedKind, NIP71_VIDEO_KINDS } from '@/constants'
-import { buildRelayPulseQueryRelayUrls } from '@/lib/home-feed-relays'
-import {
- readRelayPulseActiveNpubsCache,
- writeRelayPulseActiveNpubsCache
-} from '@/lib/relay-pulse-active-npubs-cache'
-import { hexPubkeysEqual, normalizeHexPubkey, userIdToPubkey } from '@/lib/pubkey'
-import { getPubkeysFromPTags } from '@/lib/tag'
-import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
-import { useNostr } from '@/providers/NostrProvider'
-import { queryService, replaceableEventService } from '@/services/client.service'
-import indexedDb from '@/services/indexed-db.service'
-import { registerSessionInteractivePrewarmListener } from '@/services/session-interactive-prewarm-bridge'
-import type { Event } from 'nostr-tools'
-import { kinds } from 'nostr-tools'
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
-import {
- FavoriteRelaysActivityContext,
- type TFavoriteRelaysActivityContext
-} from './favorite-relays-activity-context'
-
-const ACTIVE_WINDOW_SEC = 3600
-/** Recent slice (seconds): newest notes dominate global REQ limits; a shorter window improves author diversity. */
-const PULSE_RECENT_TAIL_SEC = 1200
-/**
- * Per-REQ event caps for the sidebar relay pulse. Keep small: each event is Schnorr-verified on the WebSocket
- * thread in nostr-tools; limits of 900+1400 caused main-thread timeouts in verifyEvent when relays returned large batches.
- */
-const PULSE_REQ_LIMIT_RECENT = 120
-const PULSE_REQ_LIMIT_EARLIER = 160
-/** Hard cap after merging two slices — enough for pubkey diversity without megabytes of verification work. */
-const PULSE_MERGED_EVENT_CAP = 400
-const FETCH_RETRY_DELAY_MS = 2500
-/** Wall-clock cadence while the tab is visible */
-const POLL_INTERVAL_MS = 60 * 60 * 1000
-/** Keep relay pulse focused on note-like activity to avoid expensive all-kind signature verification bursts. */
-const ACTIVE_PULSE_KINDS = [
- kinds.ShortTextNote,
- kinds.Repost,
- kinds.LongFormArticle,
- kinds.Highlights,
- ExtendedKind.DISCUSSION,
- ExtendedKind.PICTURE,
- ...NIP71_VIDEO_KINDS,
- ExtendedKind.COMMENT,
- ExtendedKind.GENERIC_REPOST
-] as number[]
-
-const PULSE_QUERY_OPTS = {
- firstRelayResultGraceMs: false as const,
- eoseTimeout: 1800,
- globalTimeout: 14_000
-}
-
-function mergeRelayPulseEventsById(events: { id: string; pubkey: string; created_at: number }[]) {
- const byId = new Map()
- for (const e of events) {
- const id = e.id?.trim().toLowerCase()
- if (!id || !/^[0-9a-f]{64}$/i.test(id)) continue
- const prev = byId.get(id)
- if (!prev || e.created_at > prev.created_at) byId.set(id, e)
- }
- return [...byId.values()]
-}
-
-/**
- * One REQ with a high `limit` over a full hour mostly returns the newest notes, so a few threads can
- * exhaust the cap and hide many active npubs. Two slices (recent tail + earlier in the same hour)
- * merge by id, then we dedupe by pubkey for the widget.
- */
-async function fetchRelayPulseNoteEvents(
- urls: string[],
- anchorSec: number
-): Promise<{ pubkey: string; created_at: number; id: string }[]> {
- const sinceFull = anchorSec - ACTIVE_WINDOW_SEC
- const recentSince = anchorSec - PULSE_RECENT_TAIL_SEC
- const kinds = [...ACTIVE_PULSE_KINDS]
- const settled = await Promise.allSettled([
- queryService.fetchEvents(
- urls,
- { since: recentSince, limit: PULSE_REQ_LIMIT_RECENT, kinds },
- PULSE_QUERY_OPTS
- ),
- queryService.fetchEvents(
- urls,
- {
- since: sinceFull,
- until: recentSince,
- limit: PULSE_REQ_LIMIT_EARLIER,
- kinds
- },
- PULSE_QUERY_OPTS
- )
- ])
- const merged: { id: string; pubkey: string; created_at: number }[] = []
- for (const r of settled) {
- if (r.status === 'fulfilled') merged.push(...r.value)
- }
- const deduped = mergeRelayPulseEventsById(merged)
- deduped.sort((a, b) => b.created_at - a.created_at || a.id.localeCompare(b.id))
- return deduped.slice(0, PULSE_MERGED_EVENT_CAP)
-}
-
-function aggregatePubkeysByRecency(events: { pubkey: string; created_at: number }[]): string[] {
- const lastByPk = new Map()
- for (const e of events) {
- const prev = lastByPk.get(e.pubkey) ?? 0
- if (e.created_at > prev) lastByPk.set(e.pubkey, e.created_at)
- }
- return [...lastByPk.entries()]
- .sort((a, b) => b[1] - a[1])
- .map(([pk]) => pk)
-}
-
-function partitionByFollows(orderedPubkeys: string[], followings: string[]) {
- if (followings.length === 0) {
- return {
- followPubkeys: [] as string[],
- otherPubkeys: orderedPubkeys,
- followCount: 0,
- otherCount: orderedPubkeys.length
- }
- }
- const followSet = new Set(
- followings
- .map((p) => userIdToPubkey(p))
- .filter((hex): hex is string => !!hex && /^[0-9a-f]{64}$/i.test(hex))
- .map((hex) => hex.toLowerCase())
- )
- const followPubkeys: string[] = []
- const otherPubkeys: string[] = []
- for (const pk of orderedPubkeys) {
- const hex = normalizeHexPubkey(pk)
- if (hex.length === 64 && followSet.has(hex)) followPubkeys.push(pk)
- else otherPubkeys.push(pk)
- }
- return {
- followPubkeys,
- otherPubkeys,
- followCount: followPubkeys.length,
- otherCount: otherPubkeys.length
- }
-}
-
-export function FavoriteRelaysActivityProvider({ children }: { children: React.ReactNode }) {
- const { favoriteRelays, blockedRelays, relaySets } = useFavoriteRelays()
- const { pubkey: viewerPubkey, followListEvent, relayList, cacheRelayListEvent, httpRelayListEvent } =
- useNostr()
- const followings = useMemo(
- () => (followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []),
- [followListEvent]
- )
- const [orderedPubkeys, setOrderedPubkeys] = useState([])
- const [loading, setLoading] = useState(false)
- const [relayActivityReady, setRelayActivityReady] = useState(false)
- const [lastFetchedAtMs, setLastFetchedAtMs] = useState(null)
- const [profileKind0ByPubkey, setProfileKind0ByPubkey] = useState>({})
- const [profilesLoading, setProfilesLoading] = useState(false)
- const [activeNpubsDrawerOpen, setActiveNpubsDrawerOpen] = useState(false)
- const [fallbackFollowings, setFallbackFollowings] = useState([])
- const lastCompletedFetchAtRef = useRef(Date.now())
- /** Nostr pubkey hydrates async after reload; storage already has current account (init before React mount). */
- const viewerForPulseCache = viewerPubkey ?? storage.getCurrentAccount()?.pubkey ?? null
- const orderedPubkeysRef = useRef([])
- orderedPubkeysRef.current = orderedPubkeys
- /** After restoring from disk, ignore the first empty network result (timeouts / slow relays), then behave normally. */
- const skipFirstEmptyNetworkOverwriteRef = useRef(false)
- const favoriteRelayUrlsForPulse = useMemo(
- () => [...favoriteRelays, ...relaySets.flatMap((rs) => rs.relayUrls)],
- [favoriteRelays, relaySets]
- )
-
- const pulseQueryUrls = useMemo(
- () =>
- buildRelayPulseQueryRelayUrls({
- viewerPubkey,
- favoriteRelayUrls: favoriteRelayUrlsForPulse,
- blockedRelays,
- relayList,
- cacheRelayListEvent,
- httpRelayListEvent
- }),
- [
- viewerPubkey,
- favoriteRelayUrlsForPulse,
- blockedRelays,
- relayList,
- cacheRelayListEvent,
- httpRelayListEvent
- ]
- )
-
- const relayKey = useMemo(() => pulseQueryUrls.join('\n'), [pulseQueryUrls])
-
- const fetchActive = useCallback(async () => {
- const cacheViewer = viewerPubkey ?? storage.getCurrentAccount()?.pubkey ?? null
- const urls = pulseQueryUrls
- if (urls.length === 0) {
- setLoading(false)
- setRelayActivityReady(true)
- const now = Date.now()
- setOrderedPubkeys([])
- lastCompletedFetchAtRef.current = now
- setLastFetchedAtMs(now)
- writeRelayPulseActiveNpubsCache({
- relayKey,
- viewerPubkey: cacheViewer,
- orderedPubkeys: [],
- lastFetchedAtMs: now
- })
- return
- }
- setLoading(true)
- const anchorSec = Math.floor(Date.now() / 1000)
- try {
- const events = await fetchRelayPulseNoteEvents(urls, anchorSec)
- const now = Date.now()
- const nextPubkeys = aggregatePubkeysByRecency(events)
- const prev = orderedPubkeysRef.current
- if (
- skipFirstEmptyNetworkOverwriteRef.current &&
- nextPubkeys.length === 0 &&
- prev.length > 0
- ) {
- skipFirstEmptyNetworkOverwriteRef.current = false
- logger.debug('[FavoriteRelaysActivity] kept relay pulse from cache; first fetch returned empty')
- } else {
- skipFirstEmptyNetworkOverwriteRef.current = false
- setOrderedPubkeys(nextPubkeys)
- lastCompletedFetchAtRef.current = now
- setLastFetchedAtMs(now)
- writeRelayPulseActiveNpubsCache({
- relayKey,
- viewerPubkey: cacheViewer,
- orderedPubkeys: nextPubkeys,
- lastFetchedAtMs: now
- })
- }
- } catch (error) {
- logger.debug('[FavoriteRelaysActivity] fetch failed', { error })
- if (pulseQueryUrls.length > 0) {
- setTimeout(() => void fetchRef.current(), FETCH_RETRY_DELAY_MS)
- }
- } finally {
- setLoading(false)
- setRelayActivityReady(true)
- }
- }, [relayKey, viewerPubkey, pulseQueryUrls])
-
- const fetchRef = useRef(fetchActive)
- fetchRef.current = fetchActive
-
- /** Reset pulse state when account or relay set changes so we show loading until fresh data. */
- const resetForRefetch = useCallback(() => {
- skipFirstEmptyNetworkOverwriteRef.current = false
- setRelayActivityReady(false)
- setOrderedPubkeys([])
- setProfileKind0ByPubkey({})
- }, [])
-
- /** Initial fetch on mount and when relay set changes. Use stale-while-revalidate: keep previous
- * data visible until new fetch completes instead of clearing and showing skeleton. */
- const prevRelayKeyRef = useRef(undefined)
- useEffect(() => {
- if (prevRelayKeyRef.current === undefined) {
- prevRelayKeyRef.current = relayKey
- void fetchRef.current()
- return
- }
- if (prevRelayKeyRef.current === relayKey) return
- prevRelayKeyRef.current = relayKey
- void fetchRef.current()
- }, [relayKey])
-
- /** Logged-in user changed — refetch for the new account. Follow list changes update partition via useMemo. */
- const prevViewerRef = useRef(undefined)
- useEffect(() => {
- if (prevViewerRef.current !== undefined && prevViewerRef.current !== viewerPubkey) {
- resetForRefetch()
- setFallbackFollowings([])
- void fetchRef.current()
- }
- prevViewerRef.current = viewerPubkey ?? undefined
- }, [viewerPubkey, resetForRefetch])
-
- /** Restore last successful relay-pulse author list from localStorage (same relay set + viewer). */
- useEffect(() => {
- const row = readRelayPulseActiveNpubsCache(relayKey, viewerForPulseCache)
- if (!row) return
- setOrderedPubkeys(row.orderedPubkeys)
- setLastFetchedAtMs(row.lastFetchedAtMs)
- setRelayActivityReady(true)
- lastCompletedFetchAtRef.current = row.lastFetchedAtMs
- skipFirstEmptyNetworkOverwriteRef.current = row.orderedPubkeys.length > 0
- }, [relayKey, viewerForPulseCache])
-
- /** When follow list from context is empty but we have a logged-in viewer, try IndexedDB cache.
- * Fixes race where pulse data arrives before NostrProvider has hydrated follow list from cache. */
- useEffect(() => {
- if (!viewerPubkey || followings.length > 0) {
- setFallbackFollowings((prev) => (prev.length ? [] : prev))
- return
- }
- let cancelled = false
- indexedDb
- .getReplaceableEvent(viewerPubkey, kinds.Contacts)
- .then((evt) => {
- if (cancelled || !evt) return
- setFallbackFollowings(getPubkeysFromPTags(evt.tags))
- })
- .catch(() => {})
- return () => {
- cancelled = true
- }
- }, [viewerPubkey, followings.length])
-
- /** After session interactive prewarm, relay URLs / follow context are stable — refresh pulse once. */
- useEffect(() => {
- return registerSessionInteractivePrewarmListener(() => {
- void fetchRef.current()
- })
- }, [])
-
- /** While the document is visible: poll once per hour; when returning after a long background, catch up if due. */
- useEffect(() => {
- let intervalId: ReturnType | undefined
-
- const runTick = () => {
- void fetchRef.current()
- }
-
- const syncPolling = () => {
- if (document.visibilityState !== 'visible') {
- if (intervalId !== undefined) {
- clearInterval(intervalId)
- intervalId = undefined
- }
- return
- }
- if (intervalId === undefined) {
- intervalId = setInterval(runTick, POLL_INTERVAL_MS)
- }
- if (Date.now() - lastCompletedFetchAtRef.current >= POLL_INTERVAL_MS) {
- runTick()
- }
- }
-
- syncPolling()
- document.addEventListener('visibilitychange', syncPolling)
- return () => {
- document.removeEventListener('visibilitychange', syncPolling)
- if (intervalId !== undefined) clearInterval(intervalId)
- }
- }, [])
-
- const profileFetchKeys = useMemo(() => {
- if (!viewerPubkey) return orderedPubkeys
- return orderedPubkeys.filter((pk) => !hexPubkeysEqual(pk, viewerPubkey))
- }, [orderedPubkeys, viewerPubkey])
-
- useEffect(() => {
- if (profileFetchKeys.length === 0) {
- setProfileKind0ByPubkey({})
- setProfilesLoading(false)
- return
- }
- let cancelled = false
- setProfilesLoading(true)
- ;(async () => {
- try {
- const events = await replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays(
- profileFetchKeys,
- kinds.Metadata
- )
- if (cancelled) return
- const next: Record = {}
- profileFetchKeys.forEach((pk, i) => {
- const e = events[i]
- if (e) next[pk] = e
- })
- setProfileKind0ByPubkey(next)
- } catch (err) {
- logger.debug('[FavoriteRelaysActivity] profile batch failed', { err })
- if (!cancelled) setProfileKind0ByPubkey({})
- } finally {
- if (!cancelled) setProfilesLoading(false)
- }
- })()
- return () => {
- cancelled = true
- }
- }, [profileFetchKeys])
-
- const displayPubkeys = useMemo(() => {
- if (!viewerPubkey) return orderedPubkeys
- return orderedPubkeys.filter((pk) => !hexPubkeysEqual(pk, viewerPubkey))
- }, [orderedPubkeys, viewerPubkey])
-
- const effectiveFollowings = followings.length > 0 ? followings : fallbackFollowings
- const { followPubkeys, otherPubkeys, followCount, otherCount } = useMemo(
- () => partitionByFollows(displayPubkeys, effectiveFollowings),
- [displayPubkeys, effectiveFollowings]
- )
-
- const pubkeys = useMemo(
- () => [...followPubkeys, ...otherPubkeys],
- [followPubkeys, otherPubkeys]
- )
-
- const value: TFavoriteRelaysActivityContext = useMemo(
- () => ({
- followPubkeys,
- otherPubkeys,
- followCount,
- otherCount,
- pubkeys,
- totalCount: displayPubkeys.length,
- loading,
- relayActivityReady,
- lastFetchedAtMs,
- profileKind0ByPubkey,
- profilesLoading,
- activeNpubsDrawerOpen,
- setActiveNpubsDrawerOpen,
- refetch: fetchActive
- }),
- [
- followPubkeys,
- otherPubkeys,
- followCount,
- otherCount,
- pubkeys,
- displayPubkeys.length,
- loading,
- relayActivityReady,
- lastFetchedAtMs,
- profileKind0ByPubkey,
- profilesLoading,
- activeNpubsDrawerOpen,
- fetchActive
- ]
- )
-
- return {children}
-}
diff --git a/src/providers/FeedProvider.test.ts b/src/providers/FeedProvider.test.ts
index 167dbd2f..3d9ebef0 100644
--- a/src/providers/FeedProvider.test.ts
+++ b/src/providers/FeedProvider.test.ts
@@ -1,10 +1,8 @@
import { describe, expect, it } from 'vitest'
-import { FAST_READ_RELAY_URLS } from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr'
-import { buildRelayPulseQueryRelayUrls, buildAllFavoritesFeedRelayUrls, stripNostrLandAggrFromRelayUrls } from '@/lib/home-feed-relays'
+import { buildAllFavoritesFeedRelayUrls, stripNostrLandAggrFromRelayUrls } from '@/lib/home-feed-relays'
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
-import type { Event } from 'nostr-tools'
describe('home feed relay policy', () => {
it('keeps aggr.nostr.land out of the main home feed', () => {
@@ -50,37 +48,4 @@ describe('home feed relay policy', () => {
])
expect(stripped).toEqual(['wss://relay.example/'])
})
-
- it('relay pulse stack excludes global fast-read and aggr', () => {
- const nineReadTags: string[][] = Array.from({ length: 9 }, (_, i) => [
- 'r',
- `wss://many-${i}.example/`,
- 'read'
- ])
- const oversizedCacheList = {
- kind: 10012,
- tags: [...nineReadTags],
- content: '',
- created_at: 0,
- pubkey: 'a'.repeat(64),
- id: 'b'.repeat(64),
- sig: 'c'.repeat(128)
- } satisfies Event
-
- const urls = buildRelayPulseQueryRelayUrls({
- viewerPubkey: 'd'.repeat(64),
- favoriteRelayUrls: ['wss://fav.example/'],
- blockedRelays: [],
- relayList: { read: ['wss://nip65.example/'], httpRead: ['https://http-index.example/'] },
- cacheRelayListEvent: oversizedCacheList,
- httpRelayListEvent: null
- })
-
- for (const u of FAST_READ_RELAY_URLS) {
- expect(urls).not.toContain(u)
- }
- expect(urls).not.toContain(AGGR_NOSTR_LAND_WSS)
- expect(urls).not.toContain('wss://aggr.nostr.land/')
- expect(urls.filter((u) => u.startsWith('wss://many-')).length).toBe(8)
- })
})
diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx
index 6a2475db..01b52642 100644
--- a/src/providers/FeedProvider.tsx
+++ b/src/providers/FeedProvider.tsx
@@ -266,10 +266,13 @@ export function FeedProvider({ children }: { children: ReactNode }) {
return (
({
+ relayUrls,
+ replyRelayUrls
+ }),
+ [relayUrls, replyRelayUrls]
+ )}
>
{children}
diff --git a/src/providers/favorite-relays-activity-context.tsx b/src/providers/favorite-relays-activity-context.tsx
deleted file mode 100644
index 3c06370b..00000000
--- a/src/providers/favorite-relays-activity-context.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { Event } from 'nostr-tools'
-import { createContext, useContext } from 'react'
-
-export type TFavoriteRelaysActivityContext = {
- /** Active pubkeys you follow, most recent global activity first within this group */
- followPubkeys: string[]
- /** Active pubkeys you do not follow */
- otherPubkeys: string[]
- followCount: number
- otherCount: number
- /** `followPubkeys` then `otherPubkeys` */
- pubkeys: string[]
- totalCount: number
- loading: boolean
- /** True after at least one fetch has finished (so empty state is meaningful) */
- relayActivityReady: boolean
- /** Wall-clock ms when the last sample completed; null before first fetch */
- lastFetchedAtMs: number | null
- /** Kind 0 events loaded for active pubkeys (viewer excluded); used for avatars + drawer */
- profileKind0ByPubkey: Record
- profilesLoading: boolean
- activeNpubsDrawerOpen: boolean
- setActiveNpubsDrawerOpen: (open: boolean) => void
- refetch: () => void
-}
-
-export const FavoriteRelaysActivityContext = createContext<
- TFavoriteRelaysActivityContext | undefined
->(undefined)
-
-export function useFavoriteRelaysActivity(): TFavoriteRelaysActivityContext {
- const ctx = useContext(FavoriteRelaysActivityContext)
- if (!ctx) {
- throw new Error('useFavoriteRelaysActivity must be used within FavoriteRelaysActivityProvider')
- }
- return ctx
-}
diff --git a/src/services/session-interactive-prewarm-bridge.ts b/src/services/session-interactive-prewarm-bridge.ts
index efdeac6f..a79c4df3 100644
--- a/src/services/session-interactive-prewarm-bridge.ts
+++ b/src/services/session-interactive-prewarm-bridge.ts
@@ -1,6 +1,6 @@
/**
* Multicast hook for {@link ClientService.runSessionPrewarm}'s **interactive** phase (IndexedDB @-mention
- * index + NIP-66). Widgets that depend on a settled relay/follow picture (live activities, relay pulse,
+ * index + NIP-66). Widgets that depend on a settled relay/follow picture (live activities,
* sidebar calendar) can register here so they refresh once without waiting for the follow-graph background pass.
*/