Browse Source

implement http polling

imwald
Silberengel 4 weeks ago
parent
commit
07ecafad12
  1. 7
      src/components/Explore/ExploreRelayReviews.tsx
  2. 3
      src/components/GifPicker/index.tsx
  3. 5
      src/components/MemePicker/index.tsx
  4. 6
      src/components/PostEditor/PostRelaySelector.tsx
  5. 9
      src/components/RssArticleWebBookmarks/index.tsx
  6. 9
      src/constants.ts
  7. 5
      src/hooks/useFetchCalendarRsvps.tsx
  8. 11
      src/lib/favorites-feed-relays.ts
  9. 11
      src/pages/primary/NoteListPage/FollowingFeed.tsx
  10. 3
      src/pages/primary/SpellsPage/CreateSpellDialog.tsx
  11. 15
      src/pages/primary/SpellsPage/index.tsx
  12. 9
      src/pages/secondary/FollowSetsSettingsPage/index.tsx
  13. 11
      src/pages/secondary/NoteListPage/index.tsx
  14. 13
      src/providers/LiveActivitiesProvider.tsx
  15. 217
      src/services/client.service.ts
  16. 3
      src/services/note-stats.service.ts

7
src/components/Explore/ExploreRelayReviews.tsx

@ -3,7 +3,10 @@ import { Skeleton } from '@/components/ui/skeleton'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { getRelayUrlFromRelayReviewEvent } from '@/lib/event-metadata' import { getRelayUrlFromRelayReviewEvent } from '@/lib/event-metadata'
import { getRelayUrlsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays' import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { appendCuratedReadOnlyRelays } from '@/pages/primary/SpellsPage/fauxSpellFeeds' import { appendCuratedReadOnlyRelays } from '@/pages/primary/SpellsPage/fauxSpellFeeds'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
@ -65,7 +68,7 @@ export default function ExploreRelayReviews() {
getRelayUrlsWithFavoritesFastReadAndInbox( getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
{ {
userWriteRelays: relayList?.write ?? [], userWriteRelays: relayList?.write ?? [],
maxRelays: EXPLORE_REVIEWS_MAX_RELAYS, maxRelays: EXPLORE_REVIEWS_MAX_RELAYS,

3
src/components/GifPicker/index.tsx

@ -10,6 +10,7 @@ import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { ExtendedKind, FAST_WRITE_RELAY_URLS, GIF_RELAY_URLS } from '@/constants' import { ExtendedKind, FAST_WRITE_RELAY_URLS, GIF_RELAY_URLS } from '@/constants'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -60,7 +61,7 @@ export default function GifPicker({
const fileInputRef = useRef<HTMLInputElement | null>(null) const fileInputRef = useRef<HTMLInputElement | null>(null)
const gifbuddyPopupRef = useRef<Window | null>(null) const gifbuddyPopupRef = useRef<Window | null>(null)
const userReadRelays = relayList?.read ?? [] const userReadRelays = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const userWriteRelays = relayList?.write ?? [] const userWriteRelays = relayList?.write ?? []
/** Paste / upload: GIF discovery relays + user writes (unchanged). */ /** Paste / upload: GIF discovery relays + user writes (unchanged). */

5
src/components/MemePicker/index.tsx

@ -10,6 +10,7 @@ import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { ExtendedKind, GIF_RELAY_URLS } from '@/constants' import { ExtendedKind, GIF_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
@ -22,7 +23,7 @@ import {
} from '@/services/meme.service' } from '@/services/meme.service'
import mediaUpload from '@/services/media-upload.service' import mediaUpload from '@/services/media-upload.service'
import { ExternalLink, X } from 'lucide-react' import { ExternalLink, X } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
@ -79,7 +80,7 @@ export default function MemePicker({
const fileInputRef = useRef<HTMLInputElement | null>(null) const fileInputRef = useRef<HTMLInputElement | null>(null)
const memeamigoPopupRef = useRef<Window | null>(null) const memeamigoPopupRef = useRef<Window | null>(null)
const userReadRelays = relayList?.read ?? [] const userReadRelays = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const userWriteRelays = relayList?.write ?? [] const userWriteRelays = relayList?.write ?? []
const loadMemes = useCallback( const loadMemes = useCallback(

6
src/components/PostEditor/PostRelaySelector.tsx

@ -13,6 +13,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { getRelayListFromEvent } from '@/lib/event-metadata' import { getRelayListFromEvent } from '@/lib/event-metadata'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import { Check, ChevronDown, Server } from 'lucide-react' import { Check, ChevronDown, Server } from 'lucide-react'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
@ -60,6 +61,7 @@ export default function PostRelaySelector({
useCurrentRelays() // Keep this hook call for any side effects useCurrentRelays() // Keep this hook call for any side effects
const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays() const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays()
const { pubkey, relayList } = useNostr() const { pubkey, relayList } = useNostr()
const userReadRelaysForSelection = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const [selectedRelayUrls, setSelectedRelayUrls] = useState<string[]>([]) const [selectedRelayUrls, setSelectedRelayUrls] = useState<string[]>([])
const [selectableRelays, setSelectableRelays] = useState<string[]>([]) const [selectableRelays, setSelectableRelays] = useState<string[]>([])
const [relayTypes, setRelayTypes] = useState<Record<string, RelaySourceType>>({}) const [relayTypes, setRelayTypes] = useState<Record<string, RelaySourceType>>({})
@ -220,7 +222,7 @@ export default function PostRelaySelector({
const result = await relaySelectionService.selectRelays({ const result = await relaySelectionService.selectRelays({
userWriteRelays, userWriteRelays,
userHttpWriteRelays: relayList?.httpWrite ?? [], userHttpWriteRelays: relayList?.httpWrite ?? [],
userReadRelays: relayList?.read || [], userReadRelays: userReadRelaysForSelection,
favoriteRelays: memoizedFavoriteRelays, favoriteRelays: memoizedFavoriteRelays,
blockedRelays: memoizedBlockedRelays, blockedRelays: memoizedBlockedRelays,
relaySets: memoizedRelaySets, relaySets: memoizedRelaySets,
@ -328,7 +330,7 @@ export default function PostRelaySelector({
const result = await relaySelectionService.selectRelays({ const result = await relaySelectionService.selectRelays({
userWriteRelays, userWriteRelays,
userHttpWriteRelays: relayList?.httpWrite ?? [], userHttpWriteRelays: relayList?.httpWrite ?? [],
userReadRelays: relayList?.read || [], userReadRelays: userReadRelaysForSelection,
favoriteRelays: memoizedFavoriteRelays, favoriteRelays: memoizedFavoriteRelays,
blockedRelays: memoizedBlockedRelays, blockedRelays: memoizedBlockedRelays,
relaySets: memoizedRelaySets, relaySets: memoizedRelaySets,

9
src/components/RssArticleWebBookmarks/index.tsx

@ -5,7 +5,10 @@ import { Separator } from '@/components/ui/separator'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { createWebBookmarkDraftEvent } from '@/lib/draft-event' import { createWebBookmarkDraftEvent } from '@/lib/draft-event'
import { getRelayUrlsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays' import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { showPublishingError } from '@/lib/publishing-feedback' import { showPublishingError } from '@/lib/publishing-feedback'
import { import {
@ -40,11 +43,11 @@ export default function RssArticleWebBookmarks({ articleUrl }: { articleUrl: str
}, [canonical]) }, [canonical])
const relayUrls = useMemo(() => { const relayUrls = useMemo(() => {
const read = relayList?.read ?? [] const read = userReadRelaysWithHttp(relayList)
const base = getRelayUrlsWithFavoritesFastReadAndInbox(favoriteRelays, blockedRelays, read, {}) const base = getRelayUrlsWithFavoritesFastReadAndInbox(favoriteRelays, blockedRelays, read, {})
if (!base.length) return [] if (!base.length) return []
return appendCuratedReadOnlyRelays(base, blockedRelays) return appendCuratedReadOnlyRelays(base, blockedRelays)
}, [favoriteRelays, blockedRelays, relayList?.read]) }, [favoriteRelays, blockedRelays, relayList])
const [mine, setMine] = useState<Event[]>([]) const [mine, setMine] = useState<Event[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)

9
src/constants.ts

@ -93,6 +93,15 @@ export const RELAY_NIP42_PUBLISH_ACK_TIMEOUT_MS = 90_000
/** Multi-relay queries and timeline initial REQ: after the first event, wait this long then close (query) or finalize EOSE (live feed) while keeping the subscription open for new events. */ /** Multi-relay queries and timeline initial REQ: after the first event, wait this long then close (query) or finalize EOSE (live feed) while keeping the subscription open for new events. */
export const FIRST_RELAY_RESULT_GRACE_MS = 2000 export const FIRST_RELAY_RESULT_GRACE_MS = 2000
/**
* Timelines that include HTTP index relays: interval between periodic `query()` polls while the WebSocket
* subscription stays open (HTTP relays do not receive live `EVENT` over REQ).
*/
export const HTTP_TIMELINE_POLL_INTERVAL_MS = 45_000
/** Subtracted from the polling `since` cursor so borderline events are not missed between polls. */
export const HTTP_TIMELINE_POLL_SINCE_OVERLAP_SEC = 120
/** Legacy name: was used to cap spell NoteList skeleton time; loading now ends on EOSE / first events / safety timeouts. Kept for forks. */ /** Legacy name: was used to cap spell NoteList skeleton time; loading now ends on EOSE / first events / safety timeouts. Kept for forks. */
export const SPELL_FEED_LOADING_MAX_MS = 1000 export const SPELL_FEED_LOADING_MAX_MS = 1000

5
src/hooks/useFetchCalendarRsvps.tsx

@ -8,6 +8,7 @@ import { Event } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import { FAST_READ_RELAY_URLS } from '@/constants' import { FAST_READ_RELAY_URLS } from '@/constants'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { tagNameEquals } from '@/lib/tag' import { tagNameEquals } from '@/lib/tag'
function getRsvpStatus(rsvp: Event): 'accepted' | 'tentative' | 'declined' | undefined { function getRsvpStatus(rsvp: Event): 'accepted' | 'tentative' | 'declined' | undefined {
@ -39,7 +40,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
setIsFetching(true) setIsFetching(true)
const coordinate = getReplaceableCoordinateFromEvent(calendarEvent) const coordinate = getReplaceableCoordinateFromEvent(calendarEvent)
const userRead = relayList?.read ?? [] const userRead = userReadRelaysWithHttp(relayList)
const baseUrls = new Set<string>([ const baseUrls = new Set<string>([
...FAST_READ_RELAY_URLS.map((url) => normalizeUrl(url) || url), ...FAST_READ_RELAY_URLS.map((url) => normalizeUrl(url) || url),
...userRead.map((url) => normalizeUrl(url) || url) ...userRead.map((url) => normalizeUrl(url) || url)
@ -86,7 +87,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
return () => { return () => {
cancelled = true cancelled = true
} }
}, [calendarEvent?.id, calendarEvent?.kind, calendarEvent?.pubkey, relayList?.read]) }, [calendarEvent?.id, calendarEvent?.kind, calendarEvent?.pubkey, relayList])
// When we publish an RSVP, NostrProvider calls client.emitNewEvent(event). Merge it into rsvps so the UI updates immediately. // When we publish an RSVP, NostrProvider calls client.emitNewEvent(event). Merge it into rsvps so the UI updates immediately.
useEffect(() => { useEffect(() => {

11
src/lib/favorites-feed-relays.ts

@ -25,6 +25,17 @@ const blockedSet = (blockedRelays: string[]) =>
* {@link DEFAULT_FAVORITE_RELAYS}. Same list drives the favorites tier in REQ/publish prioritization and the * {@link DEFAULT_FAVORITE_RELAYS}. Same list drives the favorites tier in REQ/publish prioritization and the
* all-favorites home feed. * all-favorites home feed.
*/ */
/**
* NIP-65 `read` plus HTTP index inboxes (kind 10243) for feed REQ / query URL lists.
*/
export function userReadRelaysWithHttp(
relayList: { read?: string[]; httpRead?: string[] } | undefined | null
): string[] {
const http = relayList?.httpRead ?? []
const read = relayList?.read ?? []
return dedupeNormalizeRelayUrlsOrdered([...http, ...read])
}
export function getFavoritesFeedRelayUrls( export function getFavoritesFeedRelayUrls(
favoriteRelays: string[], favoriteRelays: string[],
blockedRelays: string[] blockedRelays: string[]

11
src/pages/primary/NoteListPage/FollowingFeed.tsx

@ -1,6 +1,9 @@
import NormalFeed from '@/components/NormalFeed' import NormalFeed from '@/components/NormalFeed'
import type { TNoteListRef } from '@/components/NoteList' import type { TNoteListRef } from '@/components/NoteList'
import { augmentSubRequestsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays' import {
augmentSubRequestsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { buildFollowingFeedDeltaSubRequests } from '@/lib/following-feed-delta' import { buildFollowingFeedDeltaSubRequests } from '@/lib/following-feed-delta'
import { getPubkeysFromPTags } from '@/lib/tag' import { getPubkeysFromPTags } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
@ -46,12 +49,12 @@ const FollowingFeed = forwardRef<
) )
const relayReadKey = useMemo( const relayReadKey = useMemo(
() => () =>
[...(relayList?.read ?? [])] [...userReadRelaysWithHttp(relayList)]
.map((u) => normalizeUrl(u) || u) .map((u) => normalizeUrl(u) || u)
.filter(Boolean) .filter(Boolean)
.sort() .sort()
.join('\0'), .join('\0'),
[relayList?.read] [relayList]
) )
const relayWriteKey = useMemo( const relayWriteKey = useMemo(
() => () =>
@ -84,7 +87,7 @@ const FollowingFeed = forwardRef<
raw, raw,
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] } { userWriteRelays: relayList?.write ?? [] }
) )

3
src/pages/primary/SpellsPage/CreateSpellDialog.tsx

@ -24,6 +24,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { showPublishingError, showSimplePublishSuccess } from '@/lib/publishing-feedback' import { showPublishingError, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import { eventService } from '@/services/client.service' import { eventService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { getRelaysForSpellCatalogSync } from '@/services/spell.service' import { getRelaysForSpellCatalogSync } from '@/services/spell.service'
import { Info, Minus, Plus, X } from 'lucide-react' import { Info, Minus, Plus, X } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -323,7 +324,7 @@ export default function CreateSpellDialog({
const { draft, notices, pendingATags } = applyListEventToSpellDraft(base, ev) const { draft, notices, pendingATags } = applyListEventToSpellDraft(base, ev)
setForm(draft) setForm(draft)
setListImportNotices(notices) setListImportNotices(notices)
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, relayList?.read ?? [], { const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), {
userWriteRelays: relayList?.write ?? [] userWriteRelays: relayList?.write ?? []
}) })
if (pendingATags.length === 0) return if (pendingATags.length === 0) return

15
src/pages/primary/SpellsPage/index.tsx

@ -55,7 +55,8 @@ import { getPubkeysFromPTags } from '@/lib/tag'
import { formatPubkey, normalizeHexPubkey } from '@/lib/pubkey' import { formatPubkey, normalizeHexPubkey } from '@/lib/pubkey'
import { import {
augmentSubRequestsWithFavoritesFastReadAndInbox, augmentSubRequestsWithFavoritesFastReadAndInbox,
getRelayUrlsWithFavoritesFastReadAndInbox getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays' } from '@/lib/favorites-feed-relays'
import { import {
computeKind777SpellFeedSubscriptionKey, computeKind777SpellFeedSubscriptionKey,
@ -490,7 +491,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox( const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] } { userWriteRelays: relayList?.write ?? [] }
) )
if (!feedUrls.length) { if (!feedUrls.length) {
@ -598,7 +599,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
return return
} }
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, relayList?.read ?? [], { const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), {
userWriteRelays: relayList?.write ?? [] userWriteRelays: relayList?.write ?? []
}) })
const catalogAuthors = buildSpellCatalogAuthors(pubkey, contacts) const catalogAuthors = buildSpellCatalogAuthors(pubkey, contacts)
@ -760,7 +761,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
raw, raw,
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] } { userWriteRelays: relayList?.write ?? [] }
) )
try { try {
@ -843,7 +844,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox( const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
{ {
userWriteRelays: relayList?.write ?? [], userWriteRelays: relayList?.write ?? [],
applySocialKindBlockedFilter: false applySocialKindBlockedFilter: false
@ -868,7 +869,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
raw, raw,
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] } { userWriteRelays: relayList?.write ?? [] }
).map((r) => ({ ...r, reasonLabel: t('Added from follows and contact lists') })) ).map((r) => ({ ...r, reasonLabel: t('Added from follows and contact lists') }))
@ -987,7 +988,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox( const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
{ {
userWriteRelays: relayList?.write ?? [], userWriteRelays: relayList?.write ?? [],
applySocialKindBlockedFilter: fauxSpellSkipSocialKindBlocked ? false : undefined applySocialKindBlockedFilter: fauxSpellSkipSocialKindBlocked ? false : undefined

9
src/pages/secondary/FollowSetsSettingsPage/index.tsx

@ -34,7 +34,10 @@ import { randomString } from '@/lib/random'
import { showPublishingError } from '@/lib/publishing-feedback' import { showPublishingError } from '@/lib/publishing-feedback'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { getRelayUrlsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays' import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { createFollowSetDraftEvent } from '@/lib/draft-event' import { createFollowSetDraftEvent } from '@/lib/draft-event'
import { filterEventsExcludingTombstones } from '@/lib/event' import { filterEventsExcludingTombstones } from '@/lib/event'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -84,11 +87,11 @@ const FollowSetsSettingsPage = forwardRef(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox( const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] } { userWriteRelays: relayList?.write ?? [] }
) )
return appendCuratedReadOnlyRelays(feedUrls, blockedRelays) return appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
}, [favoriteRelays, blockedRelays, relayList?.read, relayList?.write]) }, [favoriteRelays, blockedRelays, relayList])
const loadLists = useCallback(async () => { const loadLists = useCallback(async () => {
if (!pubkey) { if (!pubkey) {

11
src/pages/secondary/NoteListPage/index.tsx

@ -6,7 +6,8 @@ import { Button } from '@/components/ui/button'
import { isSocialKindBlockedKind, NIP_SEARCH_DOCUMENT_KINDS, SEARCHABLE_RELAY_URLS } from '@/constants' import { isSocialKindBlockedKind, NIP_SEARCH_DOCUMENT_KINDS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { import {
augmentSubRequestsWithFavoritesFastReadAndInbox, augmentSubRequestsWithFavoritesFastReadAndInbox,
getRelayUrlsWithFavoritesFastReadAndInbox getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays' } from '@/lib/favorites-feed-relays'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { toProfileList } from '@/lib/link' import { toProfileList } from '@/lib/link'
@ -100,7 +101,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
urls: getRelayUrlsWithFavoritesFastReadAndInbox( urls: getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
readUrlOpts readUrlOpts
) )
} }
@ -143,7 +144,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
urls: getRelayUrlsWithFavoritesFastReadAndInbox( urls: getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] } { userWriteRelays: relayList?.write ?? [] }
) )
} }
@ -175,7 +176,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
raw, raw,
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] } { userWriteRelays: relayList?.write ?? [] }
) )
) )
@ -202,7 +203,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
const relayUrls = getRelayUrlsWithFavoritesFastReadAndInbox( const relayUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], userReadRelaysWithHttp(relayList),
readUrlOpts readUrlOpts
) )
const mergedReqKinds = Array.from( const mergedReqKinds = Array.from(

13
src/providers/LiveActivitiesProvider.tsx

@ -6,11 +6,20 @@ import {
resolveParentSpacesForLiveActivities, resolveParentSpacesForLiveActivities,
type TLiveActivityItem type TLiveActivityItem
} from '@/lib/live-activities' } from '@/lib/live-activities'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import client from '@/services/client.service' import client from '@/services/client.service'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { registerLiveActivitiesPrewarmCallback } from '@/services/live-activities-prewarm-bridge' import { registerLiveActivitiesPrewarmCallback } from '@/services/live-activities-prewarm-bridge'
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { useFavoriteRelays } from './FavoriteRelaysProvider' import { useFavoriteRelays } from './FavoriteRelaysProvider'
import { useFollowListOptional } from './FollowListProvider' import { useFollowListOptional } from './FollowListProvider'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
@ -47,7 +56,7 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode
const [items, setItems] = useState<TLiveActivityItem[]>([]) const [items, setItems] = useState<TLiveActivityItem[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const relayRead = relayList?.read ?? [] const relayRead = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const relayWrite = relayList?.write ?? [] const relayWrite = relayList?.write ?? []
const refresh = useCallback(async () => { const refresh = useCallback(async () => {

217
src/services/client.service.ts

@ -4,6 +4,8 @@ import {
FAST_WRITE_RELAY_URLS, FAST_WRITE_RELAY_URLS,
DOCUMENT_RELAY_URLS, DOCUMENT_RELAY_URLS,
FIRST_RELAY_RESULT_GRACE_MS, FIRST_RELAY_RESULT_GRACE_MS,
HTTP_TIMELINE_POLL_INTERVAL_MS,
HTTP_TIMELINE_POLL_SINCE_OVERLAP_SEC,
isDocumentRelayKind, isDocumentRelayKind,
isSocialKindBlockedKind, isSocialKindBlockedKind,
relayFilterIncludesDocumentRelayKind, relayFilterIncludesDocumentRelayKind,
@ -2286,6 +2288,23 @@ class ClientService extends EventTarget {
let eosedAt: number | null = null let eosedAt: number | null = null
let eventIds = new Set<string>() let eventIds = new Set<string>()
const httpTimelinePollBases = Array.from(
new Set(
relays
.filter((u) => isHttpRelayUrl(u))
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter(Boolean)
)
)
let httpPollIntervalId: ReturnType<typeof setInterval> | null = null
let httpPollCursorUnix = 0
const clearHttpTimelinePoll = () => {
if (httpPollIntervalId != null) {
clearInterval(httpPollIntervalId)
httpPollIntervalId = null
}
}
let firstResultGraceTimer: ReturnType<typeof setTimeout> | null = null let firstResultGraceTimer: ReturnType<typeof setTimeout> | null = null
const clearFirstResultGraceTimer = () => { const clearFirstResultGraceTimer = () => {
if (firstResultGraceTimer != null) { if (firstResultGraceTimer != null) {
@ -2358,6 +2377,108 @@ class ClientService extends EventTarget {
logger.warn('[ClientService] Timeline disk hydrate failed', err) logger.warn('[ClientService] Timeline disk hydrate failed', err)
} }
const applySubscribedTimelineEvent = (evt: NEvent) => {
that.addEventToCache(evt)
if (!eosedAt) {
if (eventIds.has(evt.id)) return
eventIds.add(evt.id)
events.push(evt)
flushStreamingSnapshot()
armFirstResultGraceAfterFirstEvent()
return
}
if (eventIds.has(evt.id)) return
const wallClockAtEose = eosedAt
const isBacklogStraggler = evt.created_at + TIMELINE_STRAGGLER_MAX_AGE_SEC < wallClockAtEose
if (isBacklogStraggler) {
eventIds.add(evt.id)
events.push(evt)
if (needSort) {
events = events.sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit)
}
eventIds = new Set(events.map((e) => e.id))
onEvents([...events], false)
const timeline = that.timelines[key]
if (timeline && !Array.isArray(timeline)) {
timeline.refs = events
.map((e) => [e.id, e.created_at] as TTimelineRef)
.sort((a, b) => b[1] - a[1])
that.scheduleTimelinePersist(key)
}
return
}
eventIds.add(evt.id)
onNew(evt)
const timeline = that.timelines[key]
if (!timeline || Array.isArray(timeline)) {
return
}
if (timeline.refs.length === 0) {
timeline.refs = events.map((e) => [e.id, e.created_at] as TTimelineRef).sort((a, b) => b[1] - a[1])
that.scheduleTimelinePersist(key)
return
}
let idx = 0
for (const ref of timeline.refs) {
if (evt.created_at > ref[1] || (evt.created_at === ref[1] && evt.id < ref[0])) {
break
}
if (evt.created_at === ref[1] && evt.id === ref[0]) {
return
}
idx++
}
if (idx >= timeline.refs.length) return
timeline.refs.splice(idx, 0, [evt.id, evt.created_at])
that.scheduleTimelinePersist(key)
}
const runHttpTimelinePollQuery = async (pollFilter: Filter) => {
if (httpTimelinePollBases.length === 0) return
try {
await this.query(
httpTimelinePollBases,
pollFilter,
(evt: NEvent) => {
applySubscribedTimelineEvent(evt)
},
{
firstRelayResultGraceMs: false,
globalTimeout: 25_000,
eoseTimeout: 2500
}
)
} catch (err) {
logger.debug('[ClientService] HTTP index timeline poll failed', err)
}
}
const armHttpTimelinePollingAfterInitial = () => {
clearHttpTimelinePoll()
if (httpTimelinePollBases.length === 0) return
const newestCreated = events.length > 0 ? Math.max(...events.map((e) => e.created_at)) : 0
httpPollCursorUnix = Math.max(eosedAt ?? 0, newestCreated)
httpPollIntervalId = setInterval(() => {
const base = { ...(filter as Filter) } as Filter & { until?: number }
delete base.until
const since = Math.max(0, httpPollCursorUnix - HTTP_TIMELINE_POLL_SINCE_OVERLAP_SEC)
const pollLimit = Math.min(Math.max(filter.limit ?? 200, 1), 500)
const pollFilter: Filter = { ...base, since, limit: pollLimit }
void runHttpTimelinePollQuery(pollFilter).then(() => {
httpPollCursorUnix = dayjs().unix()
})
}, HTTP_TIMELINE_POLL_INTERVAL_MS)
}
const handleTimelineEose = (eosed: boolean) => { const handleTimelineEose = (eosed: boolean) => {
if (!eosed) return if (!eosed) return
if (eosedAt != null) return if (eosedAt != null) return
@ -2367,6 +2488,7 @@ class ClientService extends EventTarget {
eosedAt = dayjs().unix() eosedAt = dayjs().unix()
if (!needSort) { if (!needSort) {
armHttpTimelinePollingAfterInitial()
return onEvents([...events], true) return onEvents([...events], true)
} }
@ -2378,7 +2500,7 @@ class ClientService extends EventTarget {
that.timelines[key] = { that.timelines[key] = {
refs: events.map((evt) => [evt.id, evt.created_at]), refs: events.map((evt) => [evt.id, evt.created_at]),
filter, filter,
urls urls: relays
} }
} else if (tl.refs.length === 0) { } else if (tl.refs.length === 0) {
tl.refs = events.map((evt) => [evt.id, evt.created_at] as TTimelineRef) tl.refs = events.map((evt) => [evt.id, evt.created_at] as TTimelineRef)
@ -2393,6 +2515,7 @@ class ClientService extends EventTarget {
tl.refs = newRefs.concat(tl.refs) tl.refs = newRefs.concat(tl.refs)
} }
} }
armHttpTimelinePollingAfterInitial()
onEvents([...events], true) onEvents([...events], true)
that.scheduleTimelinePersist(key) that.scheduleTimelinePersist(key)
} }
@ -2400,80 +2523,24 @@ class ClientService extends EventTarget {
const subCloser = this.subscribe(relays, filter, { const subCloser = this.subscribe(relays, filter, {
startLogin, startLogin,
onevent: (evt: NEvent) => { onevent: (evt: NEvent) => {
that.addEventToCache(evt) applySubscribedTimelineEvent(evt)
// not eosed yet, push to events
if (!eosedAt) {
if (eventIds.has(evt.id)) return
eventIds.add(evt.id)
events.push(evt)
flushStreamingSnapshot()
armFirstResultGraceAfterFirstEvent()
return
}
if (eventIds.has(evt.id)) return
const wallClockAtEose = eosedAt
const isBacklogStraggler =
evt.created_at + TIMELINE_STRAGGLER_MAX_AGE_SEC < wallClockAtEose
if (isBacklogStraggler) {
eventIds.add(evt.id)
events.push(evt)
if (needSort) {
events = events.sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit)
}
eventIds = new Set(events.map((e) => e.id))
onEvents([...events], false)
const timeline = that.timelines[key]
if (timeline && !Array.isArray(timeline)) {
timeline.refs = events
.map((e) => [e.id, e.created_at] as TTimelineRef)
.sort((a, b) => b[1] - a[1])
that.scheduleTimelinePersist(key)
}
return
}
eventIds.add(evt.id)
onNew(evt)
const timeline = that.timelines[key]
if (!timeline || Array.isArray(timeline)) {
return
}
if (timeline.refs.length === 0) {
timeline.refs = events.map((e) => [e.id, e.created_at] as TTimelineRef).sort((a, b) => b[1] - a[1])
that.scheduleTimelinePersist(key)
return
}
let idx = 0
for (const ref of timeline.refs) {
if (evt.created_at > ref[1] || (evt.created_at === ref[1] && evt.id < ref[0])) {
break
}
if (evt.created_at === ref[1] && evt.id === ref[0]) {
return
}
idx++
}
if (idx >= timeline.refs.length) return
timeline.refs.splice(idx, 0, [evt.id, evt.created_at])
that.scheduleTimelinePersist(key)
}, },
oneose: handleTimelineEose, oneose: handleTimelineEose,
onclose: onClose onclose: onClose
}, },
relayReqLog) relayReqLog)
if (httpTimelinePollBases.length > 0) {
const backfillFilter = { ...(filter as Filter) } as Filter & { until?: number }
delete backfillFilter.until
void runHttpTimelinePollQuery(backfillFilter)
}
return { return {
timelineKey: key, timelineKey: key,
closer: () => { closer: () => {
clearFirstResultGraceTimer() clearFirstResultGraceTimer()
clearHttpTimelinePoll()
onEvents = () => {} onEvents = () => {}
onNew = () => {} onNew = () => {}
subCloser.close() subCloser.close()
@ -2602,8 +2669,19 @@ class ClientService extends EventTarget {
} = {} } = {}
) { ) {
const originalDedupedRelays = Array.from(new Set(urls)) const originalDedupedRelays = Array.from(new Set(urls))
let relays = originalDedupedRelays.filter((url) => !isHttpRelayUrl(url)) const httpRelayBases = Array.from(
if (relays.length === 0) relays = [...FAST_READ_RELAY_URLS] new Set(
originalDedupedRelays
.filter((u) => isHttpRelayUrl(u))
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter(Boolean)
)
)
const wsOriginal = originalDedupedRelays.filter((url) => !isHttpRelayUrl(url))
let relays = [...wsOriginal]
if (relays.length === 0 && httpRelayBases.length === 0) {
relays = [...FAST_READ_RELAY_URLS]
}
const filters = Array.isArray(filter) ? filter : [filter] const filters = Array.isArray(filter) ? filter : [filter]
relays = withDocumentRelayUrlsForFilters(relays, filters) relays = withDocumentRelayUrlsForFilters(relays, filters)
const stripSocialBlockedRelays = const stripSocialBlockedRelays =
@ -2612,10 +2690,11 @@ class ClientService extends EventTarget {
if (stripSocialBlockedRelays) { if (stripSocialBlockedRelays) {
const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url)) const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url))
relays = relaysAfterSocialKindBlockedStrip(originalDedupedRelays, stripped) relays = relaysAfterSocialKindBlockedStrip(wsOriginal, stripped)
} }
relays = this.relayUrlsAfterStrikesOrRecover(relays) relays = this.relayUrlsAfterStrikesOrRecover(relays)
const events = await this.queryService.query(relays, filter, onevent, { const queryRelays = dedupeNormalizeRelayUrlsOrdered([...relays, ...httpRelayBases])
const events = await this.queryService.query(queryRelays, filter, onevent, {
eoseTimeout, eoseTimeout,
globalTimeout, globalTimeout,
firstRelayResultGraceMs, firstRelayResultGraceMs,

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

@ -23,6 +23,7 @@ import {
getWebExternalReactionTargetUrl, getWebExternalReactionTargetUrl,
rssArticleStableEventId rssArticleStableEventId
} from '@/lib/rss-article' } from '@/lib/rss-article'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { getEmojiInfosFromEmojiTags, getFirstHexEventIdFromETags, tagNameEquals } from '@/lib/tag' import { getEmojiInfosFromEmojiTags, getFirstHexEventIdFromETags, tagNameEquals } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import client, { eventService } from '@/services/client.service' import client, { eventService } from '@/services/client.service'
@ -285,7 +286,7 @@ class NoteStatsService {
client.fetchRelayList(event.pubkey), client.fetchRelayList(event.pubkey),
new Promise<{ read?: string[] }>((r) => setTimeout(() => r({}), 2000)) new Promise<{ read?: string[] }>((r) => setTimeout(() => r({}), 2000))
]) ])
;(relayList?.read ?? []).slice(0, 10).forEach(add) userReadRelaysWithHttp(relayList).slice(0, 10).forEach(add)
} catch { } catch {
// ignore // ignore
} }

Loading…
Cancel
Save