From 5093d4bd3b24f6670ca16f60e59d46a24173282d Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 11 May 2026 19:38:17 +0200 Subject: [PATCH] more refactoring --- CITATION_TEST_CONTENT.adoc | 115 ---- CITATION_TEST_README.md | 80 --- FILES_TO_UPDATE.md | 139 ----- LOGGING.md | 71 +-- MIGRATION_GUIDE.md | 189 ------- PROXY_SETUP.md | 502 +----------------- README.md | 14 +- REFACTORING_COMPLETE.md | 160 ------ REFACTORING_PLAN.md | 80 --- scripts/README-deploy.md | 2 +- .../ActiveRelaysTitlebarButton.tsx | 30 +- .../ConnectedRelaysSidebarStrip.tsx | 30 +- src/components/Embedded/EmbeddedNote.tsx | 4 +- src/components/NoteList/index.tsx | 146 +++-- .../PostEditor/PostRelaySelector.tsx | 17 - src/components/Relay/index.tsx | 57 +- src/components/SessionRelaysTab/index.tsx | 68 +-- src/constants.ts | 11 - src/features/feed/runtime.test.ts | 16 + src/features/feed/runtime.ts | 35 ++ src/hooks/useContainerWidth.ts | 28 - src/hooks/useProfileZapPollParticipation.tsx | 93 ---- src/hooks/useRelayConnectionRows.ts | 23 +- src/hooks/useViewerInboxRelayUrls.ts | 30 ++ src/hooks/useViewerInboxRelayUrlsAndAggr.ts | 46 -- src/i18n/locales/cs.ts | 13 +- src/i18n/locales/de.ts | 11 +- src/i18n/locales/en.ts | 13 +- src/i18n/locales/es.ts | 13 +- src/i18n/locales/fr.ts | 13 +- src/i18n/locales/nl.ts | 13 +- src/i18n/locales/pl.ts | 13 +- src/i18n/locales/ru.ts | 13 +- src/i18n/locales/tr.ts | 13 +- src/i18n/locales/zh.ts | 13 +- src/lib/favorites-feed-relays.ts | 43 +- src/lib/index-relay-http.ts | 17 +- src/lib/live-activities.ts | 12 - src/lib/nostr-land-aggr.ts | 93 +--- src/lib/rss-article.ts | 8 - src/lib/url.ts | 6 +- src/pages/primary/RelayPage/index.tsx | 4 +- src/pages/secondary/RelayPage/index.tsx | 4 +- src/services/client-query.service.ts | 38 +- src/services/client.service.ts | 73 +-- src/services/mention-event-search.service.ts | 10 - src/services/note-stats.service.ts | 2 +- ...trike.ts => relay-notice-fetch-failure.ts} | 10 +- 48 files changed, 330 insertions(+), 2104 deletions(-) delete mode 100644 CITATION_TEST_CONTENT.adoc delete mode 100644 CITATION_TEST_README.md delete mode 100644 FILES_TO_UPDATE.md delete mode 100644 MIGRATION_GUIDE.md delete mode 100644 REFACTORING_COMPLETE.md delete mode 100644 REFACTORING_PLAN.md delete mode 100644 src/hooks/useContainerWidth.ts delete mode 100644 src/hooks/useProfileZapPollParticipation.tsx create mode 100644 src/hooks/useViewerInboxRelayUrls.ts delete mode 100644 src/hooks/useViewerInboxRelayUrlsAndAggr.ts rename src/services/{relay-notice-strike.ts => relay-notice-fetch-failure.ts} (69%) diff --git a/CITATION_TEST_CONTENT.adoc b/CITATION_TEST_CONTENT.adoc deleted file mode 100644 index e881df15..00000000 --- a/CITATION_TEST_CONTENT.adoc +++ /dev/null @@ -1,115 +0,0 @@ -= Test Article: Citation Embedding Examples -Author Name -2024-01-15 - -This article demonstrates all citation types and display methods that can be embedded in AsciiDoc articles. - -IMPORTANT: Replace all placeholder nevent IDs with actual citation event IDs from your Nostr relays. - -== Citation Format - -All citations use the format: `[[citation::TYPE::NEVENT_ID]]` - -The TYPE can be: -- `inline` - renders inline within text -- `foot` - creates a footnote -- `foot-end` - creates a footnote that links to an endnote -- `end` - appears at the end in references section -- `quote` - block-level citation card -- `prompt-inline` - inline prompt citation -- `prompt-end` - prompt citation in references section - -== Internal Citations (Kind 30) - -Internal citations reference other Nostr events. - -=== Inline Internal Citation - -Here's an inline citation: [[citation::inline::nevent1qvzqqqqqrcpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsqgqpl98djyt2eln4uy4dlx2l6a6eyum8acgqz3vfnqptkx54suyyn5u59v88]] - -You can have multiple inline citations in one sentence: The first citation [[citation::inline::nevent1qvzqqqqqrcpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyry7njqtmu366utn3422xt84hv6pg3h5vsac602xne63hw7hmrjew8x37w3]] and the second citation [[citation::inline::nevent1qvzqqqqqrcpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyr977llj62ttqp3zw5dhdp3mdswng5ge7hfgdsz2vc7f5w5889w857hmzhr]] both reference Nostr events. - -=== Footnote Internal Citation - -This sentence has a footnote citation.footnote: [[citation::foot::nevent1qvzqqqqqrcpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsqgqpl98djyt2eln4uy4dlx2l6a6eyum8acgqz3vfnqptkx54suyyn5u59v88]] - -=== Endnote Internal Citation - -This paragraph uses an endnote citation that will appear in the references section [[citation::end::nevent1qvzqqqqqrcpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyry7njqtmu366utn3422xt84hv6pg3h5vsac602xne63hw7hmrjew8x37w3]]. - -=== Block Quote Internal Citation - -For block-level display of citations: - -[[citation::quote::nevent1qvzqqqqqrcpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyry7njqtmu366utn3422xt84hv6pg3h5vsac602xne63hw7hmrjew8x37w3]] - -== External Web Citations (Kind 31) - -External citations reference web resources. - -=== Inline External Citation - -Here's an inline external citation: [[citation::inline::nevent1qvzqqqqqrupzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyqsdgj88q2tswy2stc9p5xfaf200kr9le8m75se084upkrqkex9yvwk8faj]] - -=== Footnote-End External Citation - -This creates a footnote that links to an endnote.footnote: [[[citation::foot-end::nevent1qvzqqqqqrupzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyqsdgj88q2tswy2stc9p5xfaf200kr9le8m75se084upkrqkex9yvwk8faj]]] - -=== Endnote External Citation - -This paragraph references a web source [[citation::end::nevent1qvzqqqqqrupzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyqsdgj88q2tswy2stc9p5xfaf200kr9le8m75se084upkrqkex9yvwk8faj]]. - -== Hardcopy Citations (Kind 32) - -Hardcopy citations reference printed materials like books and journals. - -=== Inline Hardcopy Citation - -Here's an inline hardcopy citation: [[citation::inline::nevent1qvzqqqqqyqpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcprfmhxue69uhkvun9v4kxz7fwwdhhvcnfwshxsmmnwshsqgyzg2dv4w5dpsalmm28qvn3t0gsl09u0m5ar4jfupzkrt5t0fh2vgzych2c]] - -=== Endnote Hardcopy Citation - -This references a book [[citation::end::nevent1qvzqqqqqyqpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcprfmhxue69uhkvun9v4kxz7fwwdhhvcnfwshxsmmnwshsqgyzg2dv4w5dpsalmm28qvn3t0gsl09u0m5ar4jfupzkrt5t0fh2vgzych2c]]. - -=== Block Quote Hardcopy Citation - -For important book references: - -[[citation::quote::nevent1qvzqqqqqyqpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcprfmhxue69uhkvun9v4kxz7fwwdhhvcnfwshxsmmnwshsqgyzg2dv4w5dpsalmm28qvn3t0gsl09u0m5ar4jfupzkrt5t0fh2vgzych2c]] - -== Prompt Citations (Kind 33) - -Prompt citations reference AI/LLM interactions. - -=== Inline Prompt Citation - -Here's an inline prompt citation: [[citation::prompt-inline::nevent1qvzqqqqqyypzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyprf7ddefkkvdedvredu83pqqn7payvvcnrqp2s72zrx823x0wpezqzpst2]] - -=== Endnote Prompt Citation - -This paragraph discusses AI-generated content [[citation::prompt-end::nevent1qvzqqqqqyypzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyprf7ddefkkvdedvredu83pqqn7payvvcnrqp2s72zrx823x0wpezqzpst2]]. - -== Mixed Citation Usage - -You can mix different citation types in the same paragraph. For example, this sentence references both an external source [[citation::inline::nevent1qvzqqqqqrupzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyqsdgj88q2tswy2stc9p5xfaf200kr9le8m75se084upkrqkex9yvwk8faj]] and an internal Nostr event [[citation::inline::nevent1qvzqqqqqrcpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsqgqpl98djyt2eln4uy4dlx2l6a6eyum8acgqz3vfnqptkx54suyyn5u59v88]]. - -Here's a combination with footnotes and endnotes: This sentence has a footnote.footnote: [[[citation::foot::nevent1qqst8cju0m99ner9ucsu0fw3p0p4v8x0nctvmfx03u67welhnevent1qvzqqqqqrcpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyry7njqtmu366utn3422xt84hv6pg3h5vsac602xne63hw7hmrjew8x37w34z5h27wwp4m8x7ttswf0lk2wr8gs4lw9z34vamnwvaz7tmwdaehgu3wvfhkummwvaz7tmjv4kxz7fwdehk6k6]]] and this sentence references an endnote [[citation::end::nevent1qvzqqqqqrcpzqez7hqy2ca5f7z94zslmu7489zd645hrhurfeqwj5g4q6we438qcqydhwumn8ghj7mmjd3uj6un9d3shjtnfd4mkzmry9ejh2tcpr3mhxue69uhhg6r9vd5hgctyv4kzumn0wd68yvfwvdhk6tcqyry7njqtmu366utn3422xt84hv6pg3h5vsac602xne63hw7hmrjew8x37w3]]. - -== Citation Display Types Summary - -. *Inline* (`inline`, `prompt-inline`): Renders inline within the text as clickable citation text -. *Footnotes* (`foot`): Creates superscript numbers that link to footnotes -. *Foot-End* (`foot-end`): Creates footnotes that link to endnotes at the end -. *Endnotes* (`end`, `prompt-end`): References appear at the end of the document in a references section -. *Quotes* (`quote`): Block-level citation cards for emphasis - -== How to Use This Test Document - -. Replace all placeholder `nevent1qq...` IDs with actual citation event IDs from your Nostr relays -. Create citations using the Post Editor for kinds 30, 31, 32, and 33 -. Copy the nevent ID (or note ID) of your created citation event -. Replace the placeholder IDs in this document -. Publish as an AsciiDoc article (kind 30818) or Wiki Article (kind 30817) -. Verify all citation types render correctly - -NOTE: All citation event IDs in this document are placeholder examples. You must replace them with real citation event IDs to test properly. diff --git a/CITATION_TEST_README.md b/CITATION_TEST_README.md deleted file mode 100644 index 8fa739d7..00000000 --- a/CITATION_TEST_README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Citation Test Content Guide - -## File: `CITATION_TEST_CONTENT.adoc` - -This file contains comprehensive test examples for embedding all citation types in AsciiDoc articles. - -## Citation Format - -All citations use plain format (passthrough markers are added automatically during processing): - -``` -[[citation::TYPE::NEVENT_ID]] -``` - -## Citation Types Tested - -### 1. Internal Citations (Kind 30) -- `inline` - Inline citation within text -- `foot` - Footnote citation -- `end` - Endnote in references section -- `quote` - Block-level citation card - -### 2. External Web Citations (Kind 31) -- `inline` - Inline citation -- `foot-end` - Footnote linking to endnote -- `end` - Endnote in references - -### 3. Hardcopy Citations (Kind 32) -- `inline` - Inline citation -- `end` - Endnote in references -- `quote` - Block-level citation card - -### 4. Prompt Citations (Kind 33) -- `prompt-inline` - Inline prompt citation -- `prompt-end` - Prompt citation in references section - -## How to Use - -1. **Create Citation Events**: Use the Post Editor to create citations: - - Internal Citation (kind 30) - - External Citation (kind 31) - - Hardcopy Citation (kind 32) - - Prompt Citation (kind 33) - -2. **Get Citation IDs**: After creating citations, copy their nevent IDs (or note IDs) - -3. **Replace Placeholders**: In the test document, replace all `nevent1qq...` placeholder IDs with your actual citation event IDs - -4. **Test in Article**: - - Create a new AsciiDoc article (kind 30818) or Wiki Article (kind 30817) - - Paste the test content (with real citation IDs) - - Publish and verify all citation types render correctly - -## Citation Display Types - -- **inline** / **prompt-inline**: Renders as clickable text inline -- **foot**: Creates superscript footnote numbers -- **foot-end**: Creates footnotes that link to endnotes -- **end** / **prompt-end**: Appears in References section at end -- **quote**: Block-level citation card for emphasis - -## Testing Checklist - -- [ ] Internal citations render inline -- [ ] Internal citations render as footnotes -- [ ] Internal citations appear in references section -- [ ] External citations render correctly -- [ ] Hardcopy citations render correctly -- [ ] Prompt citations render correctly (inline and end) -- [ ] Block quote citations display as cards -- [ ] Mixed citations in same paragraph work -- [ ] All citation types are clickable and navigate correctly - -## Notes - -- Citation IDs can be in format: `nevent1...`, `note1...`, or hex IDs -- All citations must exist on your Nostr relays to render properly -- Endnotes automatically collect at the end in a "References" section -- Footnotes appear at the bottom of the page/section - diff --git a/FILES_TO_UPDATE.md b/FILES_TO_UPDATE.md deleted file mode 100644 index 166b9709..00000000 --- a/FILES_TO_UPDATE.md +++ /dev/null @@ -1,139 +0,0 @@ -# Files That Should Use Central Services - -## Summary -After refactoring `client.service.ts` into focused services, these files should be updated to use the new central services instead of direct client.service calls or bypassing the service layer. - -## High Priority Updates - -### 1. `src/hooks/useFetchProfile.tsx` -**Current**: Uses `client.getProfileFromIndexedDB()` and `client.fetchProfile()` -**Should Use**: `replaceableEventService.fetchReplaceableEvent()` or new ProfileService -**Benefit**: Gets cache-warming and refresh benefits - -### 2. `src/hooks/useFetchEvent.tsx` -**Current**: Directly accesses `client.eventCacheMap` (line 26) -**Should Use**: `eventService.fetchEvent()` and `eventService.getSessionEventsMatchingSearch()` -**Benefit**: Proper encapsulation, better caching - -### 3. `src/components/Note/PublicationIndex/PublicationIndex.tsx` -**Current**: -- Directly uses `indexedDb.getReplaceableEvent()` (line 686) -- Uses `client.fetchEvent()` (line 707) -- Has custom `fetchEventFromRelay()` function -**Should Use**: -- `replaceableEventService.fetchReplaceableEvent()` -- `eventService.fetchEvent()` -- `queryService.fetchEvents()` instead of custom relay fetching -**Benefit**: Consistent caching and race-based fetching - -### 4. `src/services/note-stats.service.ts` -**Current**: Uses `client.fetchEvents()` (line 128) -**Should Use**: `queryService.fetchEvents()` -**Benefit**: Race-based fetching, better performance - -### 5. `src/components/Profile/ProfileBookmarksAndHashtags.tsx` -**Current**: -- Uses `client.fetchEvents()` directly (line 292) -- Uses `client.fetchInterestListEvent()` (line 300) -**Should Use**: -- `queryService.fetchEvents()` -- `replaceableEventService.fetchReplaceableEvent(pubkey, 10015)` -**Benefit**: Consistent query strategies - -### 6. `src/components/SimpleNoteFeed/index.tsx` -**Current**: Uses `client.fetchEvents()` (line 89) -**Should Use**: `queryService.fetchEvents()` -**Benefit**: Race-based fetching for better performance - -## Medium Priority Updates - -### 7. `src/services/mention-event-search.service.ts` -**Current**: Likely uses `client.getSessionEventsMatchingSearch()` -**Should Use**: `eventService.getSessionEventsMatchingSearch()` -**Benefit**: Proper service encapsulation - -### 8. `src/components/Bookstr/BookstrContent.tsx` -**Current**: Uses `client.fetchBookstrEvents()` -**Should Use**: `macroService.fetchMacroEvents()` (with type='bookstr') -**Benefit**: Uses new MacroService architecture - -### 9. `src/services/relay-selection.service.ts` -**Current**: Uses `client.fetchRelayList()` and `client.getSessionSuccessfulPublishRelayUrlsForRandomPool()` -**Should Use**: New RelayService (to be created) -**Benefit**: Proper relay management - -### 10. `src/providers/NostrProvider/index.tsx` -**Current**: Extensive use of `client.fetchRelayList()`, `client.fetchEvents()`, etc. -**Should Use**: All new services -**Benefit**: Cache-warming integration, better performance - -## Low Priority (Internal Services) - -### 11. `src/services/gif.service.ts` -**Check**: If it uses `client.fetchEvents()` directly -**Should Use**: `queryService.fetchEvents()` - -### 12. `src/services/lightning.service.ts` -**Check**: If it fetches events directly -**Should Use**: Appropriate service - -### 13. `src/components/Embedded/EmbeddedNote.tsx` -**Check**: If it uses `client.fetchEvent()` directly -**Should Use**: `eventService.fetchEvent()` - -## Cache Integration Opportunities - -### Files That Should Use CacheService - -1. **`src/providers/NostrProvider/index.tsx`** - - Add cache-warming on login - - Use `cacheService.warmupCache()` in initialization - - Use `cacheService.getProfileWithRefresh()` for profiles - - Use `cacheService.getRelayListWithRefresh()` for relay lists - -2. **`src/hooks/useFetchProfile.tsx`** - - Use `cacheService.getProfileWithRefresh()` instead of manual cache checking - - Gets automatic background refresh for stale profiles - -3. **`src/hooks/useFetchRelayList.tsx`** - - Use `cacheService.getRelayListWithRefresh()` instead of manual cache checking - -## Direct IndexedDB Access to Replace - -### Files Accessing IndexedDB Directly (Should Use Services) - -1. **`src/components/Note/PublicationIndex/PublicationIndex.tsx`** - - Line 686: `indexedDb.getReplaceableEvent()` → Use `replaceableEventService` - - Line 930: `indexedDb.getPublicationEvent()` → Use appropriate service - - Line 934: `indexedDb.getEventFromPublicationStore()` → Use `eventService` - -2. **`src/components/Profile/index.tsx`** - - Check for direct IndexedDB access for payment info - - Should use `replaceableEventService.fetchReplaceableEvent(pubkey, ExtendedKind.PAYMENT_INFO)` - -## Migration Order - -1. **Phase 1**: Update hooks (`useFetchProfile`, `useFetchEvent`, `useFetchRelayList`) - - These are used everywhere, so fixing them benefits all components - -2. **Phase 2**: Update core components (`Profile`, `PublicationIndex`) - - High-impact components that users interact with frequently - -3. **Phase 3**: Update services (`note-stats`, `mention-event-search`) - - Internal services that can be updated without UI changes - -4. **Phase 4**: Update providers (`NostrProvider`) - - Add cache-warming and refresh strategies - -5. **Phase 5**: Update remaining components - - Lower priority, but should be done for consistency - -## Testing Checklist - -After migration, verify: -- [ ] Profiles load quickly (cache-first) -- [ ] Events load quickly (race-based fetching) -- [ ] Cache refreshes in background for stale data -- [ ] No duplicate network requests -- [ ] Cache-warming works on login -- [ ] Background refresh doesn't block UI diff --git a/LOGGING.md b/LOGGING.md index 6ff4ee61..93b9dcf9 100644 --- a/LOGGING.md +++ b/LOGGING.md @@ -1,39 +1,29 @@ -# Logging System +# Logging -This document describes the logging system implemented to reduce console noise and improve performance. +Imwald uses `src/lib/logger.ts` for application logs. Prefer it over direct `console.*` calls in shared code. ## Overview -The application now uses a centralized logging system that: -- Reduces console noise in production -- Provides conditional debug logging -- Improves performance by removing debug logs in production builds -- Allows developers to enable debug logging when needed +Current behavior: +- Development without debug mode: `info`, `warn`, and `error` are logged with formatted prefixes. +- Development with debug mode: `debug`, `info`, `warn`, `error`, component logs, and performance logs are available. +- Production: only `warn` and `error` are emitted, without formatted timestamp/caller strings. ## Usage -### For Developers +### Browser Console In development mode, you can control logging from the browser console: ```javascript -// Enable debug logging -imwaldDebug.enable() +imwaldLogger.setDebugMode(true) -// Disable debug logging -imwaldDebug.disable() +imwaldLogger.setDebugMode(false) -// Check current status -imwaldDebug.status() - -// Use debug logging directly -imwaldDebug.log('Debug message', data) -imwaldDebug.warn('Warning message', data) -imwaldDebug.error('Error message', data) -imwaldDebug.perf('Performance message', data) +imwaldLogger.isDebugEnabled() ``` -(`jumbleDebug` is still exposed as an alias for compatibility.) +`jumbleLogger` is still exposed as a legacy alias in development. ### For Code @@ -45,23 +35,23 @@ import logger from '@/lib/logger' // Debug logging (only shows in dev mode with debug enabled) logger.debug('Debug information', data) -// Info logging (always shows) +// Info logging (development only by default) logger.info('Important information', data) -// Warning logging (always shows) +// Warning logging logger.warn('Warning message', data) -// Error logging (always shows) +// Error logging logger.error('Error message', data) -// Performance logging (only in dev mode) +// Performance logging (development only) logger.perf('Performance metric', data) ``` ## Log Levels - **debug**: Development debugging information (disabled in production) -- **info**: Important application information (always enabled) +- **info**: Development application information - **warn**: Warning messages (always enabled) - **error**: Error messages (always enabled) - **perf**: Performance metrics (development only) @@ -74,38 +64,13 @@ The logger automatically configures itself based on: 2. **Local Storage**: `imwald-debug=true` enables debug mode (legacy: `jumble-debug=true`) 3. **Environment Variable**: `VITE_DEBUG=true` enables debug mode -## Performance Impact - -- **Production**: Debug logs are completely removed, improving performance -- **Development**: Debug logs are conditionally enabled, reducing noise -- **Console Operations**: Reduced console.log calls improve browser performance - -## Migration - -The following files have been updated to use the new logging system: - -- `src/providers/FeedProvider.tsx` - Feed initialization and switching -- `src/pages/primary/DiscussionsPage/index.tsx` - Vote counting and event fetching -- `src/services/client.service.ts` - Relay operations and circuit breaker -- `src/providers/NostrProvider/index.tsx` - Event signing and validation -- `src/components/Note/index.tsx` - Component rendering -- `src/PageManager.tsx` - Page rendering - -## Benefits - -1. **Reduced Console Noise**: Debug logs are hidden by default -2. **Better Performance**: Fewer console operations in production -3. **Developer Control**: Easy to enable debug logging when needed -4. **Consistent Logging**: Centralized logging with consistent format -5. **Production Ready**: Debug logs are completely removed in production builds - ## Debug Mode To enable debug mode: 1. **In Browser Console** (development only): ```javascript - imwaldDebug.enable() + imwaldLogger.setDebugMode(true) ``` 2. **Via Local Storage**: @@ -118,4 +83,4 @@ To enable debug mode: VITE_DEBUG=true npm run dev ``` -Debug mode will show all debug-level logs with timestamps and log levels. +Debug mode shows debug-level logs with timestamps, levels, and caller hints. diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md deleted file mode 100644 index 597933f0..00000000 --- a/MIGRATION_GUIDE.md +++ /dev/null @@ -1,189 +0,0 @@ -# Migration Guide: ClientService Refactoring - -## Overview -The `client.service.ts` (4313 lines) has been refactored into focused service modules. This guide helps migrate existing code to use the new services. - -## New Service Architecture - -### 1. QueryService (`client-query.service.ts`) -**Purpose**: Core query/subscription logic with race-based fetching - -**Key Methods**: -- `query(urls, filter, onevent, options)` - Core query with race strategies -- `subscribe(urls, filter, callbacks)` - Relay subscriptions -- `fetchEvents(urls, filter, options)` - Fetch events with caching -- `trackEventSeenOn(eventId, relay)` - Track where events were seen -- `getSeenEventRelayUrls(eventId)` - Get relays that saw an event - -**Migration**: Most internal usage, but if you're calling `query` or `subscribe` directly, use `queryService` instead. - -### 2. EventService (`client-events.service.ts`) -**Purpose**: Single event fetching and caching - -**Key Methods**: -- `fetchEvent(id)` - Fetch single event by ID -- `fetchEventForceRetry(eventId)` - Force retry fetch -- `fetchEventWithExternalRelays(eventId, externalRelays)` - Fetch with specific relays -- `addEventToCache(event)` - Add to session cache -- `getSessionEventsMatchingSearch(query, limit, allowedKinds)` - Search session cache -- `clearCaches()` - Clear all caches - -**Migration**: Replace `client.fetchEvent()` with `eventService.fetchEvent()` - -### 3. ReplaceableEventService (`client-replaceable-events.service.ts`) -**Purpose**: Replaceable events (profiles, relay lists, follow lists, etc.) - -**Key Methods**: -- `fetchReplaceableEvent(pubkey, kind, d?)` - Fetch replaceable event -- `fetchReplaceableEventsFromBigRelays(pubkeys, kind)` - Batch fetch -- `updateReplaceableEventCache(event)` - Update cache -- `clearCaches()` - Clear caches - -**Migration**: Replace `client.fetchProfileEvent()`, `client.fetchRelayListEvent()`, etc. with `replaceableEventService.fetchReplaceableEvent()` - -### 4. MacroService (`client-macro.service.ts`) -**Purpose**: Macro-specific events (Bookstr, Wikistr, etc.) - -**Key Methods**: -- `fetchMacroEvents(filters)` - Fetch macro events -- `getCachedMacroEvents(filters)` - Get from cache - -**Migration**: Replace `client.fetchBookstrEvents()` with `macroService.fetchMacroEvents()` - -### 5. CacheService (`client-cache.service.ts`) -**Purpose**: Universal cache-warming and refresh strategy - -**Key Methods**: -- `warmupCache(config, fetchFn)` - Warm up cache on login -- `scheduleRefresh(pubkey, kind, fetchFn)` - Schedule background refresh -- `getProfileWithRefresh(pubkey, fetchFn)` - Get profile with auto-refresh -- `getRelayListWithRefresh(pubkey, fetchFn)` - Get relay list with auto-refresh -- `isStale(pubkey, kind, cachedAt)` - Check if cache is stale -- `startPeriodicRefresh(refreshFn)` - Start periodic refresh - -**Migration**: Use for cache-warming on login and background refresh - -## Files That Need Updates - -### High Priority (Direct client.service usage) - -1. **`src/providers/NostrProvider/index.tsx`** - - Uses: `client.fetchRelayList()`, `client.fetchProfileEvent()`, `client.fetchEvents()` - - Update: Use `replaceableEventService`, `eventService`, `queryService` - -2. **`src/hooks/useFetchProfile.tsx`** - - Uses: `client.fetchProfile()`, `client.getProfileFromIndexedDB()` - - Update: Use `replaceableEventService` or new profile service - -3. **`src/hooks/useFetchEvent.tsx`** - - Uses: `client.fetchEvent()` - - Update: Use `eventService.fetchEvent()` - -4. **`src/hooks/useFetchRelayList.tsx`** - - Uses: `client.fetchRelayList()` - - Update: Use `replaceableEventService` or new relay service - -5. **`src/components/Profile/index.tsx`** - - Uses: `client.fetchPaymentInfoEvent()`, `client.fetchEvents()` - - Update: Use `replaceableEventService`, `queryService` - -6. **`src/components/Profile/ProfileBookmarksAndHashtags.tsx`** - - Uses: `client.fetchEvents()`, `client.fetchInterestListEvent()` - - Update: Use `queryService`, `replaceableEventService` - -### Medium Priority (Indirect usage) - -7. **`src/services/note-stats.service.ts`** - - Uses: `client.fetchEvents()` - - Update: Use `queryService.fetchEvents()` - -8. **`src/services/mention-event-search.service.ts`** - - Uses: `client.getSessionEventsMatchingSearch()` - - Update: Use `eventService.getSessionEventsMatchingSearch()` - -9. **`src/components/Bookstr/BookstrContent.tsx`** - - Uses: `client.fetchBookstrEvents()` - - Update: Use `macroService.fetchMacroEvents()` - -10. **`src/components/Note/PublicationIndex/PublicationIndex.tsx`** - - Uses: `client.fetchEvent()`, `indexedDb.getReplaceableEvent()` - - Update: Use `eventService.fetchEvent()`, `replaceableEventService` - -### Low Priority (Internal services) - -11. **`src/services/relay-selection.service.ts`** - - Uses: `client.fetchRelayList()` - - Update: Use `replaceableEventService` or new relay service - -12. **`src/services/relay-info.service.ts`** - - Uses: `client.fetchEvents()` - - Update: Use `queryService.fetchEvents()` - -## Migration Pattern - -### Before: -```typescript -import client from '@/services/client.service' - -const profile = await client.fetchProfile(pubkey) -const event = await client.fetchEvent(eventId) -const relayList = await client.fetchRelayList(pubkey) -``` - -### After: -```typescript -import { eventService, replaceableEventService } from '@/services/client.service' - -const profileEvent = await replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Metadata) -const event = await eventService.fetchEvent(eventId) -const relayListEvent = await replaceableEventService.fetchReplaceableEvent(pubkey, kinds.RelayList) -``` - -## Integration in Main ClientService - -The main `client.service.ts` will be refactored to: -1. Instantiate all sub-services -2. Delegate method calls to appropriate services -3. Maintain backward compatibility during transition -4. Gradually remove old implementations - -## Cache Warming Integration - -Add to `NostrProvider` initialization: - -```typescript -import cacheService from '@/services/client-cache.service' - -// On login/initialization -await cacheService.warmupCache({ - profilePubkeys: [account.pubkey, ...recentInteractions], - relayListPubkeys: [account.pubkey], - warmupFollowLists: true, - warmupMuteLists: true -}, { - fetchProfile: (id) => replaceableEventService.fetchReplaceableEvent(...), - fetchRelayList: (pubkey) => relayService.fetchRelayList(pubkey), - // ... -}) - -// Start periodic refresh -cacheService.startPeriodicRefresh(async (pubkey, kind) => { - await replaceableEventService.fetchReplaceableEvent(pubkey, kind) -}) -``` - -## Benefits - -1. **Performance**: Race-based fetching reduces wait times from 10-30s to 1-3s -2. **Cache efficiency**: Universal cache-warming and refresh strategy -3. **Maintainability**: Focused services are easier to understand and modify -4. **Testability**: Services can be tested independently -5. **Extensibility**: Easy to add new macro types or event types - -## Next Steps - -1. Complete remaining service extractions (ProfileService, RelayService, TimelineService) -2. Update main `client.service.ts` to orchestrate sub-services -3. Migrate high-priority files first -4. Test thoroughly -5. Remove old code once migration is complete diff --git a/PROXY_SETUP.md b/PROXY_SETUP.md index 2c87ab9b..6695bcc6 100644 --- a/PROXY_SETUP.md +++ b/PROXY_SETUP.md @@ -1,515 +1,51 @@ -# Proxy Server Setup for Production +# Proxy Setup -## Problem -The proxy server isn't working because `VITE_PROXY_SERVER` is being set to `http://localhost:8090` during the Docker build, which won't work from the browser on a remote server. +Imwald uses same-origin proxy paths in production so browsers do not need cross-origin CORS exceptions. -## Solution +## Link Preview / RSS Proxy -When building and deploying on the remote server, you need to build the Docker image with the correct build argument. +Set `VITE_PROXY_SERVER` at build time. The current client contract is: -### For Manual Docker Run Commands - -**IMPORTANT:** `VITE_PROXY_SERVER` must be set during Docker BUILD (as a build argument), NOT at runtime. It gets baked into the JavaScript bundle. - -Rebuild the Imwald image with the correct proxy URL: - -```bash -# Build with the correct proxy URL (baked into the JS bundle) -# -# IMPORTANT: Since users access via HTTPS (https://jumble.imwald.eu), -# the proxy URL MUST also use HTTPS or browsers will block it (mixed content). -# -# You have two options: -# -# Option 1: Configure HTTPS for port 8090 (requires SSL certificate) -# docker build --build-arg VITE_PROXY_SERVER=https://jumble.imwald.eu:8090 ... -# -# Option 2: Route proxy through your reverse proxy (recommended if you have nginx/Apache) -# Configure reverse proxy to route /proxy/* to http://localhost:8090/ -# The code constructs: ${proxyServer}/sites/${encodeURIComponent(url)} -# So /proxy/sites/... is forwarded to http://localhost:8090/sites/... -# Then use: https://jumble.imwald.eu/proxy -# -# Using Option 2 (recommended - route through Apache reverse proxy): -docker build \ - --build-arg VITE_PROXY_SERVER=https://jumble.imwald.eu/proxy \ - -t silberengel/imwald-jumble:12 \ - . - -# Then push to Docker Hub -docker push silberengel/imwald-jumble:12 - -# Then on the remote server, pull and restart: -docker stop imwald-jumble -docker rm imwald-jumble -docker pull silberengel/imwald-jumble:12 - -# Run with the same command (NO env vars needed for proxy - it's already in the bundle) -docker run -d \ - --name imwald-jumble \ - --network jumble-network \ - -p 0.0.0.0:32768:80 \ - --restart unless-stopped \ - silberengel/imwald-jumble:12 -``` - -**Note on Docker Network:** - -You only need to create the network once (it persists). Check if it exists first: - -```bash -# Check if network exists -docker network ls | grep jumble-network - -# If it doesn't exist, create it (only needed once) -docker network create jumble-network -``` - -### For Docker Compose - -### 1. Set Environment Variables Before Building - -```bash -# Use the reverse proxy route if you've configured Apache/nginx -export JUMBLE_PROXY_SERVER_URL="https://jumble.imwald.eu/proxy" -export JUMBLE_SOCIAL_URL="https://jumble.imwald.eu" -``` - -### 2. Rebuild the Docker Image - -```bash -docker-compose build --no-cache +```text +${VITE_PROXY_SERVER}/sites/?url= ``` -### 3. Restart the Containers +For the public deployment this is normally: ```bash -docker-compose down -docker-compose up -d +VITE_PROXY_SERVER=https://jumble.imwald.eu ``` -## How to Check if it's Working - -1. After deploying, open the browser console on `https://jumble.imwald.eu` -2. Navigate to a page with a URL that should show OpenGraph data -3. Look for `[WebService]` log messages that will show: - - Whether the proxy server is configured - - What URL is being used to fetch metadata - - Any errors (CORS, network, etc.) - -## Read-aloud / Piper TTS (same-origin `/api/piper-tts`) - -The client uses **`POST /api/piper-tts`** on the **same host** as the app (default build: `VITE_READ_ALOUD_TTS_URL=/api/piper-tts`) so the browser does not need cross-origin CORS. - -**Backend:** Wyoming Piper (`silberengel/wyoming-piper`, TCP **10200**) has no HTTP API. You need a small bridge that accepts `POST /api/piper-tts` and talks Wyoming over TCP. - -- **In this repo:** `services/piper-tts-proxy/` — HTTP server with the same JSON/WAV contract as the old aitherboard route. Build from repo root: - - `docker build -f services/piper-tts-proxy/Dockerfile -t imwald-piper-tts-proxy .` - - Run on **the same Docker network** as the `piper-tts` container so hostname **`piper-tts`** resolves, e.g.: - - `docker network create piper-stack` (once) - - `docker network connect piper-stack piper-tts` - - `docker run -d --name imwald-piper-tts-proxy --restart unless-stopped --network piper-stack -p 127.0.0.1:9876:9876 -e PIPER_TTS_HOST=piper-tts -e PIPER_TTS_PORT=10200 imwald-piper-tts-proxy` - - Alternatively, publish Wyoming on the host (`-p 127.0.0.1:10200:10200` on `piper-tts`) and run the proxy with `--network host` and `PIPER_TTS_HOST=127.0.0.1`. +Apache/nginx should route `/sites/` to the OG proxy container, before the SPA catch-all. -Add these **before** the catch-all `ProxyPass /` to the Imwald static container (same ordering as `/sites/`). Example **full** fragment (matches a typical TLS vhost; adjust `ServerName` / SSL paths as you do today): +Apache example: ```apache -ProxyPreserveHost On -ProxyRequests Off - -# WebSocket upgrade handling - CRITICAL for Nostr apps -RewriteEngine On -RewriteCond %{HTTP:Upgrade} websocket [NC] -RewriteCond %{HTTP:Connection} upgrade [NC] -RewriteRule ^/?(.*) "ws://127.0.0.1:8089/$1" [P,L] - -# OG / link preview (before catch-all) ProxyPass /sites/ http://127.0.0.1:8090/sites/ ProxyPassReverse /sites/ http://127.0.0.1:8090/sites/ - -# Read-aloud Piper — same-origin /api/piper-tts → imwald-piper-tts-proxy (HTTP) → Wyoming piper-tts:10200 (before catch-all) -ProxyPass /api/piper-tts http://127.0.0.1:9876/api/piper-tts -ProxyPassReverse /api/piper-tts http://127.0.0.1:9876/api/piper-tts - -# Static SPA (catch-all — must be last) ProxyPass / http://127.0.0.1:8089/ ProxyPassReverse / http://127.0.0.1:8089/ - -ProxyAddHeaders On -Header always set X-Forwarded-Proto "https" -Header always set X-Forwarded-Port "443" -``` - -Use the port where **`imwald-piper-tts-proxy`** listens (example **`9876`**). Reload Apache, then test: - -```bash -curl -sS -o /tmp/t.wav -w "%{http_code}\n" -H "Content-Type: application/json" \ - -d '{"text":"test","speed":1}' "https://jumble.imwald.eu/api/piper-tts" -``` - -Expect **200** and a WAV file. **Local dev:** `npm run dev` proxies `/api/piper-tts` → `http://127.0.0.1:9876` in `vite.config.ts`. - -Rebuild the Imwald image after changing `VITE_READ_ALOUD_TTS_URL`; `Dockerfile` passes `ARG`/`ENV` `VITE_READ_ALOUD_TTS_URL` into `npm run build`. - -## LanguageTool (same-origin `/api/languagetool`) - -The advanced event lab can call **`POST /v2/check`** on a self-hosted [LanguageTool](https://github.com/languagetool-org/languagetool) server. Set **`VITE_LANGUAGE_TOOL_URL=/api/languagetool`** at build time and proxy to your LT HTTP port (default **8010**). - -Apache (before the catch-all `ProxyPass /`): - -```apache -ProxyPass /api/languagetool http://127.0.0.1:8010 -ProxyPassReverse /api/languagetool http://127.0.0.1:8010 ``` -**Local dev:** `vite.config.ts` proxies `/api/languagetool` → `http://127.0.0.1:8010` with path rewrite so `/api/languagetool/v2/check` reaches LT’s `/v2/check`. +## Optional Same-Origin APIs -If `VITE_LANGUAGE_TOOL_URL` is empty, grammar hints in the lab are disabled. - -### Docker sidecars (LanguageTool + LibreTranslate) - -The repo defines optional Compose services on **host ports 8010** and **5000** (same targets as `vite.config.ts`). They use profile **`editor-tools`** so a plain `docker compose up` does not pull them unless you ask. +These are enabled by build-time URLs: ```bash -# Dev compose (with relay, etc.): start only grammar + translate -docker compose -f docker-compose.dev.yml --profile editor-tools up -d languagetool libretranslate - -# Or npm alias -npm run docker:editor-tools -``` - -Then point Vite at the proxies (e.g. **`.env.local`**): - -``` +VITE_READ_ALOUD_TTS_URL=/api/piper-tts VITE_LANGUAGE_TOOL_URL=/api/languagetool VITE_TRANSLATE_URL=/api/translate ``` -**Production:** `docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d` starts the full stack (including LanguageTool on **127.0.0.1:8010** and LibreTranslate on **127.0.0.1:5000**). Run `bash scripts/ensure-libretranslate-dirs.sh` once on the server (LibreTranslate UID **1032** on `.local-libretranslate`, Piper ONNX into `.local-piper-data` and the **`_piper-stack-data`** Docker volume when it exists). Proxy `/api/languagetool` and `/api/translate` from Apache/nginx to those ports, and bake the client with `LANGUAGE_TOOL_URL=/api/languagetool` and `TRANSLATE_URL=/api/translate` when running `./scripts/build-and-push-prod.sh`. - -**Notes:** LanguageTool’s JVM image often needs **~1–2 GiB** RAM. LibreTranslate **does not listen on port 5000 until models are ready**; without **`LT_LOAD_ONLY`** it may pull **many gigabytes** first, so the Vite proxy can show **`ECONNRESET` on `/translate`** while booting. Compose loads **`LT_LOAD_ONLY`** from **`scripts/libretranslate-lt.default.env`** (same file is read by **`scripts/ensure-libretranslate-dirs.sh`** and **`scripts/prune-libretranslate-packages.sh`**). Edit that file to add or remove codes, then recreate LibreTranslate; first start downloads packs for every listed code. For a one-off prune without editing the file, run **`export LT_LOAD_ONLY=…`** before **`npm run docker:prune-libretranslate-packages`**. **`LT_UPDATE_MODELS`** defaults to **`true`** so if you **expand** `LT_LOAD_ONLY` later, a **recreated** container still **installs missing** Argos packages into the bind-mounted `.local-libretranslate` tree (otherwise an older en/de-only cache sticks). Set **`LT_UPDATE_MODELS=false`** after everything is installed if you want faster routine restarts. Models are stored under **`.local-libretranslate/share`** and **`.local-libretranslate/cache`** (gitignored) with **bind mounts** so they survive **`docker compose down`**, image updates, and container recreate. **`scripts/ensure-libretranslate-dirs.sh`** (run automatically by **`npm run dev:all`**, **`npm run stack:remote`**, **`npm run docker:editor-tools`**, etc.) creates those dirs and **`chown`s them to UID 1032** via a short **Alpine** container so the LibreTranslate user can write. If you start **`libretranslate` by hand**, run **`npm run docker:prep-libretranslate`** once first. First download can still take **several minutes**; use **`docker logs -f jumble-libretranslate`** until **`curl http://127.0.0.1:5000/languages`** returns JSON. If logs show **`Cannot update models`** / **`Unavailable language codes: …`**, one bad token in **`LT_LOAD_ONLY` aborts the whole install** (you stay on whatever was already on disk, often en/de only). **Norwegian** must be **`nb`** (Bokmål), not ISO **`no`**. After you shrink **`LT_LOAD_ONLY`**, run **`npm run docker:prune-libretranslate-packages`** to remove leftover Argos package dirs under **`.local-libretranslate/share/argos-translate/packages`** (and unused **MiniSBD** `.onnx` files); the script briefly stops **`jumble-libretranslate`** or **`imwald-libretranslate`**. - -## LibreTranslate (same-origin `/api/translate`) - -Optional **`VITE_TRANSLATE_URL=/api/translate`** for `POST /translate` (LibreTranslate-compatible). Example Apache: +Proxy targets: ```apache +ProxyPass /api/piper-tts http://127.0.0.1:9876/api/piper-tts +ProxyPassReverse /api/piper-tts http://127.0.0.1:9876/api/piper-tts +ProxyPass /api/languagetool http://127.0.0.1:8010 +ProxyPassReverse /api/languagetool http://127.0.0.1:8010 ProxyPass /api/translate http://127.0.0.1:5000 ProxyPassReverse /api/translate http://127.0.0.1:5000 ``` -**Local dev:** `vite.config.ts` proxies `/api/translate` → `http://127.0.0.1:5000` with path rewrite. - -If `VITE_TRANSLATE_URL` is empty, translate actions in the advanced lab are hidden. - -## Update Proxy Server's ALLOW_ORIGIN - -Since users access via `https://jumble.imwald.eu`, you need to update the proxy server's `ALLOW_ORIGIN`: - -```bash -# Stop the proxy container -docker stop imwald-jumble-proxy -docker rm imwald-jumble-proxy - -# Restart with correct ALLOW_ORIGIN (must match how users access the frontend) -docker run -d \ - --name imwald-jumble-proxy \ - --network jumble-network \ - -p 0.0.0.0:8090:8080 \ - -e ALLOW_ORIGIN=https://jumble.imwald.eu \ - -e ENABLE_PPROF=true \ - --restart unless-stopped \ - ghcr.io/danvergara/jumble-proxy-server:latest -``` - -## HTTPS Certificate Setup for Port 8090 - -Since users access via `https://jumble.imwald.eu`, you need HTTPS for the proxy to avoid mixed content errors. - -**Option A: SSL Certificate for Port 8090** - -If you want to access the proxy directly on port 8090 with HTTPS, you'll need an SSL certificate: - -```bash -# Using Let's Encrypt with certbot -certbot certonly --standalone -d jumble.imwald.eu --expand - -# Then configure your reverse proxy or the proxy server itself to use the certificate -# The exact steps depend on your setup (nginx, Apache, or direct in the proxy container) -``` - -**Option B: Route Through Reverse Proxy (Recommended)** - -If you already have a reverse proxy (nginx/Apache) handling HTTPS for `jumble.imwald.eu`, route the proxy through it: - -### Apache Reverse Proxy Setup - -1. **Enable required Apache modules:** -```bash -sudo a2enmod proxy -sudo a2enmod proxy_http -sudo a2enmod rewrite -sudo a2enmod headers -sudo systemctl restart apache2 -``` - -2. **Add reverse proxy configuration to your Apache virtual host** (typically in `/etc/apache2/sites-available/jumble.imwald.eu-le-ssl.conf` or similar): - -```apache - - - ServerName jumble.imwald.eu - ServerAlias www.jumble.imwald.eu - - # Reverse Proxy Configuration - - # Proxy for the jumble-proxy-server (must come BEFORE the catch-all / rule) - # The code constructs: ${proxyServer}/sites/${encodeURIComponent(url)} - # So /proxy/sites/... needs to be forwarded to http://127.0.0.1:8090/sites/... - # IMPORTANT: Use Location block to scope headers properly for /proxy/ path only - - ProxyPreserveHost Off - ProxyPass http://127.0.0.1:8090/ - ProxyPassReverse http://127.0.0.1:8090/ - # Unset forwarded headers that might make the proxy server use Host header instead of URL path - RequestHeader unset X-Forwarded-Host - RequestHeader unset X-Forwarded-Server - RequestHeader set Host "127.0.0.1:8090" - - - # Reverse Proxy for the main Imwald app (needs Host header preserved) - ProxyPreserveHost On - ProxyPass / http://127.0.0.1:32768/ - ProxyPassReverse / http://127.0.0.1:32768/ - - # Headers for proper proxying - Header always set X-Forwarded-Proto https - Header always set X-Forwarded-Port 443 - -Include /etc/letsencrypt/options-ssl-apache.conf -SSLCertificateFile /etc/letsencrypt/live/jumble.imwald.eu/fullchain.pem -SSLCertificateKeyFile /etc/letsencrypt/live/jumble.imwald.eu/privkey.pem - - -``` - -**Important:** The code constructs URLs like `https://jumble.imwald.eu/proxy/sites/https%3A%2F%2Fexample.com`. Apache receives `/proxy/sites/https%3A%2F%2Fexample.com` and forwards it to `http://127.0.0.1:8090/sites/https%3A%2F%2Fexample.com` (strips `/proxy` prefix). - -3. **Enable the site (if not already enabled):** -```bash -sudo a2ensite jumble.imwald.eu-le-ssl.conf -``` - -4. **Reload Apache:** -```bash -sudo apache2ctl configtest # Check for errors first -sudo systemctl reload apache2 -``` - -5. **Test the proxy route:** -```bash -# Test with a real URL - the code constructs /proxy/sites/{encoded-url} -curl https://jumble.imwald.eu/proxy/sites/https%3A%2F%2Fexample.com -# Should return example.com's HTML, NOT jumble.imwald.eu's HTML -# If you see Imwald HTML, the proxy server is using the Host header instead of the URL path -``` - -**If the test returns Imwald HTML instead of the requested site's HTML:** - -The proxy server is using the `Host` header (`jumble.imwald.eu`) to determine what to fetch. Update your Apache config to use `ProxyPreserveHost Off` for the `/proxy/` path: - -```apache -# In your Apache config, change from: -ProxyPreserveHost On -ProxyPass /proxy/ http://127.0.0.1:8090/ - -# To: -ProxyPreserveHost Off -ProxyPass /proxy/ http://127.0.0.1:8090/ -ProxyPassReverse /proxy/ http://127.0.0.1:8090/ - -# Then set it back to On for the main app: -ProxyPreserveHost On -ProxyPass / http://127.0.0.1:32768/ -``` - -Then reload Apache and test again. - -6. **Build with the proxy URL:** -```bash -docker build \ - --build-arg VITE_PROXY_SERVER=https://jumble.imwald.eu/proxy \ - -t silberengel/imwald-jumble:12 \ - . -``` - -**Note:** The proxy URL in `VITE_PROXY_SERVER` should be `https://jumble.imwald.eu/proxy` (without trailing slash), and the code will append `/sites/...` automatically. - -## Important Notes - -- The `VITE_PROXY_SERVER` value is baked into the JavaScript bundle during build time -- You MUST rebuild the Docker image if you change `VITE_PROXY_SERVER` -- The proxy server's `ALLOW_ORIGIN` must match the frontend URL users access (`https://jumble.imwald.eu`) -- Mixed content: HTTPS pages cannot load HTTP resources - both must use HTTPS -- If using direct port access (8090), you need an SSL certificate for that port - -## Troubleshooting - -### OG proxy (`docker logs …og-proxy`): `fetch failed` / `Retryable error` - -The **wikistr** image uses Node **`fetch`** to load the target URL. Inside Docker, transient DNS failures often show up as **`TypeError: fetch failed`** with cause **`getaddrinfo EAI_AGAIN`** (the proxy then retries and can spam logs). Compose sets **`dns: [1.1.1.1, 8.8.8.8]`** on **`og-proxy`** so lookups do not rely only on Docker’s internal resolver. If your network blocks third-party DNS, remove or override that **`dns`** block (compose override file or forked image). - -Quick checks: - -```bash -docker exec jumble-og-proxy getent hosts example.com -docker exec jumble-og-proxy node -e "fetch('https://example.com').then(r=>console.log('HTTP',r.status)).catch(e=>console.error(e.cause||e))" -curl -sS -o /dev/null -w '%{http_code}\n' -H 'Origin: http://localhost:5173' 'http://127.0.0.1:8090/sites/?url=https%3A%2F%2Fexample.com' -``` - -### If Proxy Returns Imwald HTML Instead of Requested Site - -If you've set `ProxyPreserveHost Off` but still get Imwald HTML, test the proxy server directly: - -**1. Test the proxy server directly (bypassing Apache):** -```bash -# Test direct connection to proxy on port 8090 -curl http://127.0.0.1:8090/sites/https%3A%2F%2Fexample.com -# Should return example.com's HTML, NOT jumble.imwald.eu's HTML -``` - -**2. Check proxy server logs:** -```bash -docker logs imwald-jumble-proxy --tail 50 -# Look for what URL the proxy is trying to fetch -# This will show if the proxy server is receiving the correct path or if it's using Host header -``` - -**2b. Check what request the proxy server is actually receiving:** -```bash -# Enable verbose logging in Apache to see what it's forwarding -# Or check what the proxy receives by making a test request and watching logs: -docker logs -f imwald-jumble-proxy & -# Then in another terminal: -curl -v https://jumble.imwald.eu/proxy/sites/https%3A%2F%2Fexample.com -# Look at the proxy logs to see what URL it tried to fetch -``` - -**3. Check if Apache is receiving `/proxy/` requests:** -```bash -# Watch Apache access log to see if /proxy/ requests reach Apache -sudo tail -f /var/log/apache2/access.log | grep proxy -# Then in another terminal, make a request: -curl https://jumble.imwald.eu/proxy/sites/https%3A%2F%2Fexample.com -# Check if the request appears in Apache logs -``` - -**3b. Check what Apache is forwarding:** -```bash -# Check Apache error log for proxy-related entries -sudo tail -f /var/log/apache2/error.log -# Then make a request and see what Apache is doing -``` - -**3c. Verify Apache config is being used:** -```bash -# Check that your Location block syntax is correct: -sudo apache2ctl -S | grep jumble.imwald.eu -# Make sure the site is enabled and config syntax is correct: -sudo apache2ctl configtest -``` - -**4. Possible Issues:** - -**IMPORTANT: If you see `Server: nginx` in response headers, nginx is in front of Apache!** - -If you see `Server: nginx/1.29.3` or `X-Powered-By: PleskLin` in the response headers, nginx reverse proxy is handling requests before Apache. You need to configure nginx directly (bypassing Plesk interface) to pass `/proxy/` to Apache, or route it directly to the proxy server. - -**If nginx is in front of Apache (bypassing Plesk interface):** - -Since nginx is handling requests before Apache, configure nginx directly by editing nginx config files. - -**Option A: Configure nginx to pass `/proxy/` through to Apache:** - -1. Find nginx config for your domain: -```bash -# Plesk usually stores configs here: -/etc/nginx/conf.d/vhost.conf -# or -/etc/nginx/conf.d/jumble.imwald.eu.conf -# or check Plesk's nginx vhosts: -/etc/nginx/plesk.conf.d/vhosts/jumble.imwald.eu.conf -``` - -2. Edit the nginx config file for `jumble.imwald.eu` - -3. Add this location block BEFORE any catch-all location: -```nginx -location /proxy/ { - # Forward to Apache (check what port Apache is listening on) - proxy_pass http://127.0.0.1:8080; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -} -``` - -**Note:** Replace `8080` with the port Apache is actually listening on. Check with: -```bash -sudo netstat -tlnp | grep apache -# or -sudo ss -tlnp | grep apache -``` - -4. Test nginx config: `sudo nginx -t` -5. Reload nginx: `sudo systemctl reload nginx` - -**Then Apache will handle it with your existing Apache config.** - -**Option B: Configure nginx to route `/proxy/` directly to the proxy server (simpler):** - -1. Edit nginx config for `jumble.imwald.eu` (same location as above) -2. Add this location block BEFORE any catch-all location: -```nginx -location /proxy/ { - proxy_pass http://127.0.0.1:8090/; - proxy_set_header Host 127.0.0.1:8090; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # Don't send X-Forwarded-Host which might confuse the proxy - proxy_set_header X-Forwarded-Host ""; -} -``` - -3. Test nginx config: `sudo nginx -t` -4. Reload nginx: `sudo systemctl reload nginx` - -This bypasses Apache entirely for `/proxy/` requests and goes directly to the proxy server. - -**Other possible issues:** -- The proxy server might be using the `X-Forwarded-Host` header instead of the URL path -- The proxy server might need a specific Host header value -- The proxy server might not be correctly parsing `/sites/...` path - -**5. Unset forwarded headers that might confuse the proxy server:** -```apache -# The proxy server might be using X-Forwarded-Host instead of the URL path -# Unset or modify these headers for the /proxy/ path: -ProxyPass /proxy/ http://127.0.0.1:8090/ -ProxyPassReverse /proxy/ http://127.0.0.1:8090/ -ProxyPreserveHost Off -# Unset forwarded headers that might interfere -RequestHeader unset X-Forwarded-Host -RequestHeader unset X-Forwarded-Server -RequestHeader set Host "127.0.0.1:8090" -``` - -### Other Console Errors - -If you see errors in the console: -- `[WebService] No proxy server configured` - `VITE_PROXY_SERVER` is undefined or empty -- `[WebService] CORS/Network error` - The proxy URL might be wrong, or CORS isn't configured -- `[WebService] Failed to fetch metadata` - The proxy server might not be running or accessible - +For the full production workflow, use `scripts/README-deploy.md` and `docker-compose.prod.yml`. diff --git a/README.md b/README.md index c041f42b..c6626b4e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@
- - - - Imwald logo - + Imwald

logo designed by Daniel David

@@ -11,17 +7,15 @@ **Maintainer: [Silberengel](https://github.com/Silberengel)** · Evolved from [Cody Tseng’s Jumble](https://github.com/CodyTseng/jumble) -A Nostr web client focused on relay feeds, discovery, and spells. **Imwald** keeps the same core ideas as upstream, with a substantial navigation and information-architecture rewrite (see below). The public instance lives at [jumble.imwald.eu](https://jumble.imwald.eu). +A Nostr web client focused on relay feeds, discovery, and spells. The public instance lives at [jumble.imwald.eu](https://jumble.imwald.eu). --- -## Major rewrite (this line) - -High-level changes versus a “stock” Jumble-style layout: +## Product Shape ### Home vs feed -- **Home** is the **Explore** experience: relay directory, **Following’s Favorites**, and related discovery — not a duplicate of your main timeline. +- **Home** is the **Explore** experience: relay directory, Following’s Favorites, and related discovery. - **Feed** is a dedicated primary area for **favorite relays**, displaying their diverse social content as a feed: short text notes (microblogging), longform articles, wiki pages, media notes, calendar entries, etc. ### RSS diff --git a/REFACTORING_COMPLETE.md b/REFACTORING_COMPLETE.md deleted file mode 100644 index 01ba1b2b..00000000 --- a/REFACTORING_COMPLETE.md +++ /dev/null @@ -1,160 +0,0 @@ -# ClientService Refactoring - Completion Summary - -## Overview -The monolithic `client.service.ts` (originally 4312 lines) has been successfully refactored into a modular architecture with focused sub-services. - -## Results - -### File Size Reduction -- **Before**: 4312 lines -- **After**: 2119 lines -- **Reduction**: 50.8% (2193 lines removed/refactored) - -### Services Created - -1. **QueryService** (`client-query.service.ts`) - 437 lines - - Core query/subscription logic - - Race-based fetching strategies (replaceableRace, immediateReturn) - - Relay connection management - - Event tracking (seenOnRelays) - - Concurrent subscription management - -2. **EventService** (`client-events.service.ts`) - 267 lines - - Single event fetching by ID (hex, note1, nevent1, naddr1) - - Event caching with DataLoader - - Session cache management - - Force retry and external relay fetching - -3. **ReplaceableEventService** (`client-replaceable-events.service.ts`) - 230 lines - - Replaceable event fetching (profiles, relay lists, follow lists, etc.) - - Batch operations with DataLoader - - Cache coordination with IndexedDB - -4. **MacroService** (`client-macro.service.ts`) - 310 lines - - Macro-specific event fetching (Bookstr, Wikistr, extensible) - - Macro metadata extraction - - Specialized filtering and verse range expansion - - Cache-first strategy with background refresh - -5. **CacheService** (`client-cache.service.ts`) - 311 lines - - Universal cache-warming strategy - - Cache refresh scheduling - - TTL management - - Background refresh coordination - -## Architecture - -### Service Dependencies -``` -ClientService (orchestrator) -├── QueryService (core query logic) -├── EventService (depends on QueryService) -├── ReplaceableEventService (depends on QueryService) -├── MacroService (depends on QueryService) -└── CacheService (standalone, used by providers) -``` - -### Delegation Pattern -The main `ClientService` now acts as an orchestrator: -- **39+ method delegations** to sub-services -- Maintains backward compatibility -- Handles complex orchestration (publishing, timeline subscriptions) -- Manages cross-cutting concerns (relay selection, profile search) - -## Key Improvements - -### 1. Performance -- **Race-based fetching**: Replaceable events use 2-second wait strategy -- **Immediate return**: Single events by ID return on first match -- **Batch operations**: DataLoader batching reduces network calls -- **Cache-first**: IndexedDB checked before network requests - -### 2. Maintainability -- **Focused services**: Each service has a single responsibility -- **Clear boundaries**: Services are testable in isolation -- **Reduced complexity**: Main service is 50% smaller -- **Better organization**: Related functionality grouped together - -### 3. Extensibility -- **MacroService**: Easy to add new macro types (Wikistr, etc.) -- **QueryService**: Centralized query logic for all event types -- **ReplaceableEventService**: Handles all replaceable event kinds uniformly - -## What Remains in ClientService - -The following responsibilities remain in `ClientService` as they represent core orchestration: - -1. **Publishing** (`publishEvent`, `determineTargetRelays`) - - Complex relay selection logic - - Publish statistics and failure tracking - - Authentication handling - -2. **Timeline Subscriptions** (`subscribeTimeline`) - - Complex state management - - Progressive loading - - Timeline reference tracking - -3. **Profile Search** (`searchProfiles`, `searchProfilesFromLocal`) - - FlexSearch index management - - Local profile search - -4. **Relay List Merging** (`fetchRelayLists`) - - Complex merging of cache relays with regular relay lists - - Offline-first strategy - -## Code Quality - -### Linter Status -- ✅ **0 errors** -- ✅ **0 warnings** -- ✅ All unused imports removed -- ✅ All unused methods removed -- ✅ All duplicate implementations removed - -### Logger Integration -- ✅ Efficient logger implementation -- ✅ Development: Browser console -- ✅ Production: Console GUI in Imwald app -- ✅ Performance logging included - -## Migration Status - -### Completed -- ✅ All sub-services created and integrated -- ✅ Main service refactored to orchestrate sub-services -- ✅ Legacy code removed -- ✅ Code cleaned and optimized - -### Remaining (Optional) -The following files could be updated to use sub-services directly (see `FILES_TO_UPDATE.md`): -- Hooks: `useFetchProfile`, `useFetchEvent`, `useFetchRelayList` -- Components: `Profile`, `PublicationIndex`, `ProfileBookmarksAndHashtags` -- Services: `note-stats.service`, `mention-event-search.service` -- Providers: `NostrProvider` (for cache-warming integration) - -These updates are **optional** as the current delegation pattern maintains backward compatibility. - -## Testing Recommendations - -1. **Unit Tests**: Test each service independently -2. **Integration Tests**: Test service interactions -3. **Performance Tests**: Verify race-based fetching improvements -4. **Cache Tests**: Verify cache-warming and refresh strategies - -## Next Steps (Optional) - -1. **Cache-Warming Integration**: Add cache-warming to `NostrProvider` on login -2. **Direct Service Usage**: Update high-priority files to use services directly -3. **Additional Services**: Consider extracting TimelineService or RelayService if needed -4. **Documentation**: Add JSDoc comments to public methods - -## Conclusion - -The refactoring is **complete and production-ready**. The codebase is now: -- ✅ **Clean**: 0 linter errors/warnings -- ✅ **Performant**: Race-based fetching, cache-first strategy -- ✅ **Robust**: Proper error handling, logging -- ✅ **Maintainable**: Focused services, clear boundaries -- ✅ **Extensible**: Easy to add new features - -The main `ClientService` now serves as a clean orchestrator, delegating to specialized sub-services while maintaining backward compatibility. diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md deleted file mode 100644 index e83eb424..00000000 --- a/REFACTORING_PLAN.md +++ /dev/null @@ -1,80 +0,0 @@ -# ClientService Refactoring Plan - -## Overview -Breaking down the 4313-line `client.service.ts` into focused, maintainable services with universal cache-warming strategy. - -## Service Architecture - -### 1. **QueryService** (`client-query.service.ts`) ✅ -- Core query/subscription logic -- Race-based fetching strategies -- Relay connection management -- Event tracking - -### 2. **CacheService** (`client-cache.service.ts`) ✅ -- Universal cache-warming strategy -- Cache refresh scheduling -- TTL management -- Background refresh coordination - -### 3. **EventService** (`client-events.service.ts`) ✅ -- Single event fetching -- Event caching -- Session cache management -- DataLoader integration - -### 4. **ReplaceableEventService** (`client-replaceable-events.service.ts`) ✅ -- Replaceable event fetching (profiles, relay lists, etc.) -- Batch operations -- Cache coordination - -### 5. **MacroService** (`client-macro.service.ts`) ✅ -- Macro-specific event fetching (Bookstr, etc.) -- Macro metadata extraction -- Specialized filtering -- Extensible for future macro types - -### 6. **CacheService** (`client-cache.service.ts`) ✅ -- Universal cache-warming strategy -- Cache refresh scheduling -- TTL management -- Background refresh coordination - -### Note on Additional Services -The following services were considered but are currently handled within `ClientService` as orchestration logic: -- **Profile search/index**: Handled in `ClientService` with delegation to `ReplaceableEventService` for fetching -- **Relay management**: Publishing and relay selection remain in `ClientService` as core orchestration -- **Timeline subscriptions**: Complex state management remains in `ClientService` but uses `QueryService` and `EventService` - -## Cache Strategy - -### Cache-Warming -- On login: Warm up current user's profile, relay list, follow list -- On feed load: Warm up profiles for visible pubkeys (batch, limited to 50) -- Background: Periodically refresh stale entries - -### Cache-Refreshing -- Stale detection: Check `addedAt` timestamp vs refresh thresholds -- Background refresh: Non-blocking, queued refresh for stale entries -- Periodic refresh: Every 5 minutes, check and refresh stale profiles - -### TTLs -- Profiles: 30 min cache, 15 min refresh threshold -- Payment info: 5 min cache, 2 min refresh threshold -- Relay lists: 15 min cache, 10 min refresh threshold -- Follow/Mute lists: 60 min cache, 30 min refresh threshold - -## Integration Strategy - -1. Create service instances in main `ClientService` -2. Inject dependencies (QueryService into others) -3. Maintain backward compatibility during transition -4. Gradually migrate methods to use new services -5. Remove old code once migration complete - -## Performance Benefits - -- **Faster initial load**: Cache-warming pre-fetches critical data -- **Better responsiveness**: Background refresh keeps cache fresh without blocking UI -- **Reduced network calls**: Smart cache invalidation prevents unnecessary fetches -- **Improved maintainability**: Focused services are easier to test and modify diff --git a/scripts/README-deploy.md b/scripts/README-deploy.md index 1622a58a..81bcc071 100644 --- a/scripts/README-deploy.md +++ b/scripts/README-deploy.md @@ -63,7 +63,7 @@ That starts the **full** stack in `docker-compose.prod.yml` (app, NIP-66 monitor Apache (or nginx) must still proxy `/api/languagetool` → `127.0.0.1:8010` and `/api/translate` → `127.0.0.1:5000`. -**Shared host:** if you already run another `og-proxy` on `127.0.0.0.1:8090` or another Wyoming Piper on the same ports, `docker compose up -d` can fail with a port or name conflict. Either stop the duplicates or start only the pieces you need, e.g. `docker compose up -d jumble jumble-nip66-monitor languagetool libretranslate` (and point `PIPER_TTS_HOST` / Apache at your existing Piper stack if applicable). +**Shared host:** if you already run another `og-proxy` on `127.0.0.1:8090` or another Wyoming Piper on the same ports, `docker compose up -d` can fail with a port or name conflict. Either stop the duplicates or start only the pieces you need, e.g. `docker compose up -d jumble jumble-nip66-monitor languagetool libretranslate` (and point `PIPER_TTS_HOST` / Apache at your existing Piper stack if applicable). Equivalent one-liner: `npm run stack:remote` diff --git a/src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx b/src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx index 9155a7b2..cf5684c6 100644 --- a/src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx +++ b/src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx @@ -19,25 +19,19 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import RelayIcon from '../RelayIcon' -function rowMuted(connected: boolean, sessionStriked: boolean) { - return !connected || sessionStriked +function rowMuted(connected: boolean) { + return !connected } -function rowTitle( - url: string, - connected: boolean, - sessionStriked: boolean, - t: (k: string) => string -) { +function rowTitle(url: string, connected: boolean, t: (k: string) => string) { const base = simplifyUrl(url) - if (sessionStriked) return `${base} — ${t('Relay session striked')}` if (!connected) return `${base} — ${t('Not connected')}` return base } /** * Same interaction pattern as {@link SeenOnButton}: Server + counts, menu lists relays with {@link RelayIcon}. - * Shows favorites + default/inbox relays; disconnected or session-striked relays are muted. + * Shows favorites + default/inbox relays; disconnected relays are muted. */ export function ActiveRelaysTitlebarButton() { const { t } = useTranslation() @@ -76,8 +70,8 @@ export function ActiveRelaysTitlebarButton() { ) - const rowClass = (connected: boolean, sessionStriked: boolean) => - cn(rowMuted(connected, sessionStriked) && 'opacity-45 text-muted-foreground') + const rowClass = (connected: boolean) => + cn(rowMuted(connected) && 'opacity-45 text-muted-foreground') if (isSmallScreen) { return ( @@ -101,12 +95,12 @@ export function ActiveRelaysTitlebarButton() { ) : null}
- {rows.map(({ url, connected, sessionStriked }) => ( + {rows.map(({ url, connected }) => ( - - )) - )} - - -

@@ -201,31 +160,6 @@ export default function SessionRelaysTab() {

- {debug.strikedUrls.length > 0 && ( -
-

- {t('Session relays all striked')} -

-
    - {debug.strikedUrls.map((url) => ( -
  • - - -
  • - ))} -
-
- )}
) } diff --git a/src/constants.ts b/src/constants.ts index 283c8a59..3e6df5a0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,9 +8,6 @@ export const IMWALD_API_BASE_URL = (import.meta.env.VITE_JUMBLE_API_BASE_URL as string | undefined)?.trim() || 'https://api.jumble.imwald.eu' -/** @deprecated Use {@link IMWALD_API_BASE_URL} */ -export const JUMBLE_API_BASE_URL = IMWALD_API_BASE_URL - /** Git Republic web UI for repository links; override with VITE_GITREPUBLIC_WEB_BASE_URL for self-hosted. */ export const GITREPUBLIC_WEB_BASE_URL = ( (import.meta.env.VITE_GITREPUBLIC_WEB_BASE_URL as string | undefined) ?? 'https://gitrepublic.imwald.eu' @@ -215,12 +212,6 @@ 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. */ -export const SPELL_FEED_LOADING_MAX_MS = 1000 - -/** @deprecated Alias of {@link SPELL_FEED_LOADING_MAX_MS}. */ -export const SPELL_FEED_FIRST_RELAY_GRACE_MS = SPELL_FEED_LOADING_MAX_MS - /** * Implicit query feed grace ({@link FIRST_RELAY_RESULT_GRACE_MS}) applies only when the largest `limit` among * filters is at least this value. Omitting `limit` counts as 0 (no implicit grace). @@ -973,8 +964,6 @@ export const ZAP_STREAM_WATCH_URL_REGEX = export const IMWALD_MAINTAINER_PUBKEY = 'f4eb8e62add1340b9cadcd9861e669b2e907cea534e0f7f3ac974c11c758a51a' -/** @deprecated Use {@link IMWALD_MAINTAINER_PUBKEY} */ -export const JUMBLE_PUBKEY = IMWALD_MAINTAINER_PUBKEY export const CODY_PUBKEY = '8125b911ed0e94dbe3008a0be48cfe5cd0c0b05923cfff917ae7e87da8400883' export const SILBERENGEL_PUBKEY = 'fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1' diff --git a/src/features/feed/runtime.test.ts b/src/features/feed/runtime.test.ts index 59065e8a..7ddb097c 100644 --- a/src/features/feed/runtime.test.ts +++ b/src/features/feed/runtime.test.ts @@ -96,4 +96,20 @@ describe('FeedRuntime', () => { expect(next.paginationStatus).toBe('exhausted') expect(next.hasMore).toBe(false) }) + + it('can seed existing rows before loading an older page', async () => { + const runtime = new FeedRuntime({ descriptorKey: 'feed-a' }) + runtime.seed([evt('new', 20)], { hasMore: true, nextCursor: 19 }) + + const next = await runtime.loadMore(async ({ cursor }) => { + expect(cursor).toBe(19) + return { + relayEvents: [evt('old', 10)], + hasMore: true + } + }) + + expect(next.rows.map((row) => row.id)).toEqual(['new', 'old']) + expect(next.paginationStatus).toBe('idle') + }) }) diff --git a/src/features/feed/runtime.ts b/src/features/feed/runtime.ts index 3da29ccf..9e9688a8 100644 --- a/src/features/feed/runtime.ts +++ b/src/features/feed/runtime.ts @@ -52,6 +52,7 @@ export type FeedRuntimeState = FeedRuntimeSnapshot & { export type FeedRuntimeAction = | { type: 'start'; descriptorKey: string; generation: number; refresh: boolean; keepRowsStale?: boolean } + | { type: 'seed'; events: Event[]; stale?: boolean; hasMore?: boolean; nextCursor?: number } | { type: 'cache'; events: Event[]; stale: boolean } | { type: 'relayBatch'; events: Event[]; relayOutcomes?: FeedRelayOutcome[]; fresh?: boolean } | { type: 'relayDone'; relayOutcomes?: FeedRelayOutcome[]; hasMore?: boolean; nextCursor?: number } @@ -83,6 +84,12 @@ export type FeedRuntimeLoadResult = { nextCursor?: number } +export type FeedRuntimeSeedOptions = { + stale?: boolean + hasMore?: boolean + nextCursor?: number +} + export type FeedRuntimeLoader = (args: { descriptorKey: string generation: number @@ -181,6 +188,19 @@ export function feedRuntimeReducer( switch (action.type) { case 'reset': return createInitialFeedRuntimeState(action.descriptorKey) + case 'seed': + return derive( + { + ...state, + rawRows: action.events, + stale: action.stale ?? false, + hasMore: action.hasMore ?? state.hasMore, + nextCursor: action.nextCursor ?? state.nextCursor, + paginationStatus: action.hasMore === false ? 'exhausted' : state.paginationStatus, + pageError: undefined + }, + options + ) case 'start': { const keepRows = action.keepRowsStale ? state.rawRows : [] return derive( @@ -292,6 +312,21 @@ export class FeedRuntime { return snapshot } + seed(events: Event[], options: FeedRuntimeSeedOptions = {}): FeedRuntimeSnapshot { + this.state = feedRuntimeReducer( + this.state, + { + type: 'seed', + events, + stale: options.stale, + hasMore: options.hasMore, + nextCursor: options.nextCursor + }, + this.options + ) + return this.snapshot() + } + async load(loader: FeedRuntimeLoader, refresh = false): Promise { this.abortController?.abort() const generation = ++this.generation diff --git a/src/hooks/useContainerWidth.ts b/src/hooks/useContainerWidth.ts deleted file mode 100644 index cd9ca0fa..00000000 --- a/src/hooks/useContainerWidth.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { RefObject, useEffect, useState } from 'react' - -/** - * Tracks the rendered width of `ref`'s element via ResizeObserver. - * Returns `undefined` until the first measurement fires. - * Use this when you need a component to respond to its *own* container width - * rather than the viewport width (e.g. inside a split-pane layout where - * `isSmallScreen` is still `false` but the column is narrow). - */ -export function useContainerWidth(ref: RefObject): number | undefined { - const [width, setWidth] = useState(undefined) - - useEffect(() => { - const el = ref.current - if (!el) return - - const observer = new ResizeObserver((entries) => { - const entry = entries[0] - if (entry) setWidth(entry.contentRect.width) - }) - observer.observe(el) - // Initialise synchronously so there's no render flash - setWidth(el.getBoundingClientRect().width) - return () => observer.disconnect() - }, [ref]) - - return width -} diff --git a/src/hooks/useProfileZapPollParticipation.tsx b/src/hooks/useProfileZapPollParticipation.tsx deleted file mode 100644 index 9dc6df86..00000000 --- a/src/hooks/useProfileZapPollParticipation.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { ExtendedKind, FAST_READ_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants' -import { - filterZapPollVoteReceiptsForVoter, - getPollIdFromZapReceipt, - parseZapPollEvent, - userZapPollVoteOption -} from '@/lib/zap-poll' -import { normalizeUrl } from '@/lib/url' -import client from '@/services/client.service' -import { Event } from 'nostr-tools' -import { kinds } from 'nostr-tools' -import { useCallback, useEffect, useMemo, useState } from 'react' - -function participationRelayUrls(): string[] { - const seen = new Set() - const out: string[] = [] - for (const u of [...SEARCHABLE_RELAY_URLS, ...FAST_READ_RELAY_URLS]) { - const n = normalizeUrl(u) || u - if (!n || seen.has(n)) continue - seen.add(n) - out.push(n) - } - return out.slice(0, 14) -} - -export type TZapPollProfileRow = { - poll: Event - voteReceipt: Event - optionIndex: number -} - -/** - * Zap poll votes by `profilePubkey` (kind 9735 with P=profile and k=6969 in embedded zap request), - * resolved to kind 6969 poll events for profile timeline merge. - */ -export function useProfileZapPollParticipation(profilePubkey: string | undefined) { - const [rows, setRows] = useState([]) - const [loading, setLoading] = useState(false) - - const load = useCallback(async () => { - if (!profilePubkey) { - setRows([]) - return - } - setLoading(true) - try { - const urls = participationRelayUrls() - const receipts = await client.fetchEvents(urls, { - kinds: [kinds.Zap], - '#p': [profilePubkey.trim().toLowerCase()], - limit: 300 - }) - const voteReceipts = filterZapPollVoteReceiptsForVoter(receipts, profilePubkey) - const pollIds = [...new Set(voteReceipts.map(getPollIdFromZapReceipt).filter(Boolean) as string[])] - if (pollIds.length === 0) { - setRows([]) - return - } - const polls = await client.fetchEvents(urls, { - kinds: [ExtendedKind.ZAP_POLL], - ids: pollIds, - limit: pollIds.length - }) - const pollById = new Map(polls.map((p) => [p.id, p])) - const built: TZapPollProfileRow[] = [] - for (const vr of voteReceipts) { - const pid = getPollIdFromZapReceipt(vr) - if (!pid) continue - const poll = pollById.get(pid) - if (!poll) continue - const pollMeta = parseZapPollEvent(poll) - if (!pollMeta) continue - const opt = userZapPollVoteOption(poll, pollMeta, profilePubkey, [vr]) - if (opt === undefined) continue - built.push({ poll, voteReceipt: vr, optionIndex: opt }) - } - built.sort((a, b) => b.voteReceipt.created_at - a.voteReceipt.created_at) - setRows(built) - } catch { - setRows([]) - } finally { - setLoading(false) - } - }, [profilePubkey]) - - useEffect(() => { - void load() - }, [load]) - - const pollIdsVoted = useMemo(() => new Set(rows.map((r) => r.poll.id)), [rows]) - - return { rows, loading, reload: load, pollIdsVoted } -} diff --git a/src/hooks/useRelayConnectionRows.ts b/src/hooks/useRelayConnectionRows.ts index bb670c7e..76c313f4 100644 --- a/src/hooks/useRelayConnectionRows.ts +++ b/src/hooks/useRelayConnectionRows.ts @@ -2,7 +2,7 @@ import { DEFAULT_FAVORITE_RELAYS, FAST_READ_RELAY_URLS } from '@/constants' import { normalizeAnyRelayUrl } from '@/lib/url' import { useNostr } from '@/providers/NostrProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' -import client, { JUMBLE_SESSION_RELAY_STRIKES_CHANGED } from '@/services/client.service' +import client from '@/services/client.service' import { useEffect, useMemo, useState } from 'react' const POLL_MS = 1500 @@ -31,18 +31,15 @@ export type TRelayConnectionRow = { url: string /** WebSocket in the pool is open. */ connected: boolean - /** Session strike threshold reached — app skips this relay for reads/publishes until cleared. */ - sessionStriked: boolean } /** * Relays to show in “active relays” UI: favorites + NIP-65 read/write + defaults + fast-read, - * then any pool-connected URL not already listed. {@link row.connected} reflects the live WebSocket; - * {@link row.sessionStriked} reflects {@link client.isSessionRelayStrikedForReads}. + * then any pool-connected URL not already listed. {@link row.connected} reflects the live WebSocket. */ export function useRelayConnectionRows(): { rows: TRelayConnectionRow[] - /** Relays that are both socket-connected and not session-striked (usable for REQs this session). */ + /** Relays that currently have an open WebSocket connection. */ connectedCount: number } { const { relayList } = useNostr() @@ -50,7 +47,6 @@ export function useRelayConnectionRows(): { const [connectedCanon, setConnectedCanon] = useState>(() => new Set(client.getConnectedRelayUrls().map(canon)) ) - const [strikesEpoch, setStrikesEpoch] = useState(0) useEffect(() => { const tick = () => setConnectedCanon(new Set(client.getConnectedRelayUrls().map(canon))) @@ -59,12 +55,6 @@ export function useRelayConnectionRows(): { return () => clearInterval(id) }, []) - useEffect(() => { - const bump = () => setStrikesEpoch((n) => n + 1) - window.addEventListener(JUMBLE_SESSION_RELAY_STRIKES_CHANGED, bump) - return () => window.removeEventListener(JUMBLE_SESSION_RELAY_STRIKES_CHANGED, bump) - }, []) - return useMemo(() => { const inbox = [...(relayList?.read ?? []), ...(relayList?.write ?? [])] const base = mergeUniquePreserveOrder( @@ -77,8 +67,7 @@ export function useRelayConnectionRows(): { const rowFor = (url: string, socketConnected: boolean): TRelayConnectionRow => ({ url, - connected: socketConnected, - sessionStriked: client.isSessionRelayStrikedForReads(url) + connected: socketConnected }) const rows: TRelayConnectionRow[] = base.map((url) => @@ -91,7 +80,7 @@ export function useRelayConnectionRows(): { rows.push(rowFor(url, true)) } - const connectedCount = rows.filter((r) => r.connected && !r.sessionStriked).length + const connectedCount = rows.filter((r) => r.connected).length return { rows, connectedCount } - }, [favoriteRelays, relayList?.read, relayList?.write, connectedCanon, strikesEpoch]) + }, [favoriteRelays, relayList?.read, relayList?.write, connectedCanon]) } diff --git a/src/hooks/useViewerInboxRelayUrls.ts b/src/hooks/useViewerInboxRelayUrls.ts new file mode 100644 index 00000000..8ace95e5 --- /dev/null +++ b/src/hooks/useViewerInboxRelayUrls.ts @@ -0,0 +1,30 @@ +import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays' +import { useNostrOptional } from '@/providers/nostr-context' +import client from '@/services/client.service' +import { useEffect, useState } from 'react' + +/** Viewer NIP-65 read inboxes (incl. HTTP read) for embed / thread fan-out. */ +export function useViewerInboxRelayUrls(): { + inboxRelayUrls: string[] +} { + const nostr = useNostrOptional() + const pk = nostr?.pubkey?.trim() + const [inboxRelayUrls, setInboxRelayUrls] = useState([]) + + useEffect(() => { + if (!pk) { + setInboxRelayUrls([]) + return + } + let cancelled = false + void client.peekRelayListFromStorage(pk).then((rl) => { + if (cancelled) return + setInboxRelayUrls(userReadRelaysWithHttp(rl).slice(0, 14)) + }) + return () => { + cancelled = true + } + }, [pk]) + + return { inboxRelayUrls } +} diff --git a/src/hooks/useViewerInboxRelayUrlsAndAggr.ts b/src/hooks/useViewerInboxRelayUrlsAndAggr.ts deleted file mode 100644 index e112fe29..00000000 --- a/src/hooks/useViewerInboxRelayUrlsAndAggr.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays' -import { viewerMayUseNostrLandAggr } from '@/lib/nostr-land-aggr' -import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' -import { useNostrOptional } from '@/providers/nostr-context' -import client from '@/services/client.service' -import type { TRelayList } from '@/types' -import { useEffect, useMemo, useState } from 'react' - -/** - * Viewer NIP-65 read inboxes (incl. HTTP read) for embed / thread fan-out, plus whether they may use - * {@link AGGR_NOSTR_LAND_WSS} (nostr.land in favorites or NIP-65 lists). - */ -export function useViewerInboxRelayUrlsAndAggrEligibility(): { - inboxRelayUrls: string[] - allowNostrLandAggr: boolean -} { - const nostr = useNostrOptional() - const pk = nostr?.pubkey?.trim() - const { favoriteRelays } = useFavoriteRelays() - const [inboxRelayUrls, setInboxRelayUrls] = useState([]) - const [peekedNip65, setPeekedNip65] = useState(null) - - useEffect(() => { - if (!pk) { - setInboxRelayUrls([]) - setPeekedNip65(null) - return - } - let cancelled = false - void client.peekRelayListFromStorage(pk).then((rl) => { - if (cancelled) return - setPeekedNip65(rl) - setInboxRelayUrls(userReadRelaysWithHttp(rl).slice(0, 14)) - }) - return () => { - cancelled = true - } - }, [pk]) - - const allowNostrLandAggr = useMemo( - () => viewerMayUseNostrLandAggr(favoriteRelays ?? [], peekedNip65 ?? undefined), - [favoriteRelays, peekedNip65] - ) - - return { inboxRelayUrls, allowNostrLandAggr } -} diff --git a/src/i18n/locales/cs.ts b/src/i18n/locales/cs.ts index 8edc0ba7..259d6102 100644 --- a/src/i18n/locales/cs.ts +++ b/src/i18n/locales/cs.ts @@ -513,7 +513,6 @@ export default { "Seen on": "Seen on", "Active relays": "Active relays", "Not connected": "Not connected", - "Relay session striked": "Skipped this session (too many connection failures)", "More relays": "+{{count}} relays", "Temporarily display this reply": "Temporarily display this reply", "Note not found": "Note not found", @@ -604,19 +603,11 @@ export default { relayType_contextual: "Reply/PM", relayType_randomly_selected: "Random (optional)", "Session relays": "Session relays", - "Session relays tab description": "Relay logic for this session: working and striked preset relays, and scored random relays (used to prefer faster, proven relays when adding random relays to publish). Striked relays are skipped for reads and publishes until reload.", + "Session relays tab description": "Relay logic for this session: preset relays and scored random relays used to prefer faster, proven relays when adding random relays to publish.", "Session relays preset working": "Working preset relays", - "Session relays preset working hint": "Preset relays (from app defaults) that have not reached the session failure threshold (2 failures).", - "Session relays preset striked": "Striked preset relays", - "Session relays preset striked hint": "Preset relays that have reached 2 connection or publish failures this session and are skipped for reads and writes until you reload the app.", + "Session relays preset working hint": "Preset relays from app defaults.", "Session relays scored random": "Scored random relays", "Session relays scored random hint": "Relays that have accepted at least one publish this session; used to prefer faster relays when picking random relays. Sorted by average latency.", - "Session relays all striked": "All striked relays (any source)", - "Session relays clear strike": "Allow again", - "Session relays clear strike hint": "Remove this relay from the session block list; it will be used again until new connection failures.", - "relaySessionStrikes.bannerWarning": "This relay has {{count}} session strike(s) (limit {{threshold}}) after connection or query failures.", - "relaySessionStrikes.bannerSkipped": "This relay has reached the session failure limit ({{threshold}} strikes) and is skipped for reads and publishes in this tab.", - "relaySessionStrikes.refreshHint": "Use {{refresh}} to clear strikes for this relay and load the feed again.", successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 2c159d6c..5ed601f7 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -533,7 +533,6 @@ export default { "Seen on": "Gesehen auf", "Active relays": "Aktive Relays", "Not connected": "Nicht verbunden", - "Relay session striked": "Diese Sitzung übersprungen (zu viele Verbindungsfehler)", "More relays": "+{{count}} Relays", "Temporarily display this reply": "Antwort vorübergehend anzeigen", "Note not found": "Die Notiz wurde nicht gefunden", @@ -626,17 +625,9 @@ export default { "Session relays": "Session-Relays", "Session relays tab description": "Relay-Logik für diese Session: funktionierende und gestrichene Preset-Relays sowie bewertete Zufallsrelays. Gestrichene Relays werden für Lesen und Schreiben bis zum Neuladen der App übersprungen.", "Session relays preset working": "Funktionierende Preset-Relays", - "Session relays preset working hint": "Preset-Relays (App-Standard), die die Session-Fehlerschwelle (2 Fehler) noch nicht erreicht haben.", - "Session relays preset striked": "Gestrichene Preset-Relays", - "Session relays preset striked hint": "Preset-Relays mit 2 Verbindungs- oder Publish-Fehlern in dieser Session; werden für Lesen und Schreiben bis zum Neuladen übersprungen.", + "Session relays preset working hint": "Preset-Relays aus den App-Standards.", "Session relays scored random": "Bewertete Zufallsrelays", "Session relays scored random hint": "Relays, die in dieser Session mindestens ein Publish angenommen haben; werden beim Auswählen von Zufallsrelays bevorzugt. Sortiert nach durchschnittlicher Latenz.", - "Session relays all striked": "Alle gestrichenen Relays (alle Quellen)", - "Session relays clear strike": "Wieder zulassen", - "Session relays clear strike hint": "Relay aus der Session-Sperrliste nehmen; es wird wieder genutzt, bis neue Verbindungsfehler auftreten.", - "relaySessionStrikes.bannerWarning": "Dieses Relay hat {{count}} Session-Strike(s) (Limit {{threshold}}) nach Verbindungs- oder Abfragefehlern.", - "relaySessionStrikes.bannerSkipped": "Dieses Relay hat die Session-Fehlergrenze ({{threshold}} Strikes) erreicht und wird in diesem Tab für Lesen und Publizieren übersprungen.", - "relaySessionStrikes.refreshHint": "Mit {{refresh}} werden die Strikes für dieses Relay zurückgesetzt und der Feed erneut geladen.", successes: "Erfolge", None: "Keine", "Cache & offline storage": "Cache & Offline-Speicher", diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index b1a5643c..ae452004 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -537,7 +537,6 @@ export default { "Seen on": "Seen on", "Active relays": "Active relays", "Not connected": "Not connected", - "Relay session striked": "Skipped this session (too many connection failures)", "More relays": "+{{count}} relays", "Temporarily display this reply": "Temporarily display this reply", "Note not found": "Note not found", @@ -628,19 +627,11 @@ export default { relayType_contextual: "Reply/PM", relayType_randomly_selected: "Random (optional)", "Session relays": "Session relays", - "Session relays tab description": "Relay logic for this session: working and striked preset relays, and scored random relays (used to prefer faster, proven relays when adding random relays to publish). Striked relays are skipped for reads and publishes until reload.", + "Session relays tab description": "Relay logic for this session: preset relays and scored random relays used to prefer faster, proven relays when adding random relays to publish.", "Session relays preset working": "Working preset relays", - "Session relays preset working hint": "Preset relays (from app defaults) that have not reached the session failure threshold (2 failures).", - "Session relays preset striked": "Striked preset relays", - "Session relays preset striked hint": "Preset relays that have reached 2 connection or publish failures this session and are skipped for reads and writes until you reload the app.", + "Session relays preset working hint": "Preset relays from app defaults.", "Session relays scored random": "Scored random relays", "Session relays scored random hint": "Relays that have accepted at least one publish this session; used to prefer faster relays when picking random relays. Sorted by average latency.", - "Session relays all striked": "All striked relays (any source)", - "Session relays clear strike": "Allow again", - "Session relays clear strike hint": "Remove this relay from the session block list; it will be used again until new connection failures.", - "relaySessionStrikes.bannerWarning": "This relay has {{count}} session strike(s) (limit {{threshold}}) after connection or query failures.", - "relaySessionStrikes.bannerSkipped": "This relay has reached the session failure limit ({{threshold}} strikes) and is skipped for reads and publishes in this tab.", - "relaySessionStrikes.refreshHint": "Use {{refresh}} to clear strikes for this relay and load the feed again.", successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 27919b0a..7ea0f6cb 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -513,7 +513,6 @@ export default { "Seen on": "Visto en", "Active relays": "Active relays", "Not connected": "Not connected", - "Relay session striked": "Skipped this session (too many connection failures)", "More relays": "+{{count}} relays", "Temporarily display this reply": "Mostrar temporalmente esta respuesta", "Note not found": "No se encontró la nota", @@ -604,19 +603,11 @@ export default { relayType_contextual: "Reply/PM", relayType_randomly_selected: "Random (optional)", "Session relays": "Session relays", - "Session relays tab description": "Relay logic for this session: working and striked preset relays, and scored random relays (used to prefer faster, proven relays when adding random relays to publish).", + "Session relays tab description": "Relay logic for this session: preset relays and scored random relays used to prefer faster, proven relays when adding random relays to publish.", "Session relays preset working": "Working preset relays", - "Session relays preset working hint": "Preset relays (from app defaults) that have not reached 3 publish failures this session.", - "Session relays preset striked": "Striked preset relays", - "Session relays preset striked hint": "Preset relays that have reached 3 publish failures this session and are skipped for the rest of the session.", + "Session relays preset working hint": "Preset relays from app defaults.", "Session relays scored random": "Scored random relays", "Session relays scored random hint": "Relays that have accepted at least one publish this session; used to prefer faster relays when picking random relays. Sorted by average latency.", - "Session relays all striked": "All striked relays (any source)", - "Session relays clear strike": "Allow again", - "Session relays clear strike hint": "Remove this relay from the session block list; it will be used again until new connection failures.", - "relaySessionStrikes.bannerWarning": "This relay has {{count}} session strike(s) (limit {{threshold}}) after connection or query failures.", - "relaySessionStrikes.bannerSkipped": "This relay has reached the session failure limit ({{threshold}} strikes) and is skipped for reads and publishes in this tab.", - "relaySessionStrikes.refreshHint": "Use {{refresh}} to clear strikes for this relay and load the feed again.", successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 3fa925b5..4a98186c 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -513,7 +513,6 @@ export default { "Seen on": "Vu sur", "Active relays": "Active relays", "Not connected": "Not connected", - "Relay session striked": "Skipped this session (too many connection failures)", "More relays": "+{{count}} relays", "Temporarily display this reply": "Afficher temporairement cette réponse", "Note not found": "Note introuvable", @@ -604,19 +603,11 @@ export default { relayType_contextual: "Reply/PM", relayType_randomly_selected: "Random (optional)", "Session relays": "Session relays", - "Session relays tab description": "Relay logic for this session: working and striked preset relays, and scored random relays (used to prefer faster, proven relays when adding random relays to publish).", + "Session relays tab description": "Relay logic for this session: preset relays and scored random relays used to prefer faster, proven relays when adding random relays to publish.", "Session relays preset working": "Working preset relays", - "Session relays preset working hint": "Preset relays (from app defaults) that have not reached 3 publish failures this session.", - "Session relays preset striked": "Striked preset relays", - "Session relays preset striked hint": "Preset relays that have reached 3 publish failures this session and are skipped for the rest of the session.", + "Session relays preset working hint": "Preset relays from app defaults.", "Session relays scored random": "Scored random relays", "Session relays scored random hint": "Relays that have accepted at least one publish this session; used to prefer faster relays when picking random relays. Sorted by average latency.", - "Session relays all striked": "All striked relays (any source)", - "Session relays clear strike": "Allow again", - "Session relays clear strike hint": "Remove this relay from the session block list; it will be used again until new connection failures.", - "relaySessionStrikes.bannerWarning": "This relay has {{count}} session strike(s) (limit {{threshold}}) after connection or query failures.", - "relaySessionStrikes.bannerSkipped": "This relay has reached the session failure limit ({{threshold}} strikes) and is skipped for reads and publishes in this tab.", - "relaySessionStrikes.refreshHint": "Use {{refresh}} to clear strikes for this relay and load the feed again.", successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", diff --git a/src/i18n/locales/nl.ts b/src/i18n/locales/nl.ts index f58276c4..9cd7fbcb 100644 --- a/src/i18n/locales/nl.ts +++ b/src/i18n/locales/nl.ts @@ -513,7 +513,6 @@ export default { "Seen on": "Seen on", "Active relays": "Active relays", "Not connected": "Not connected", - "Relay session striked": "Skipped this session (too many connection failures)", "More relays": "+{{count}} relays", "Temporarily display this reply": "Temporarily display this reply", "Note not found": "Note not found", @@ -604,19 +603,11 @@ export default { relayType_contextual: "Reply/PM", relayType_randomly_selected: "Random (optional)", "Session relays": "Session relays", - "Session relays tab description": "Relay logic for this session: working and striked preset relays, and scored random relays (used to prefer faster, proven relays when adding random relays to publish). Striked relays are skipped for reads and publishes until reload.", + "Session relays tab description": "Relay logic for this session: preset relays and scored random relays used to prefer faster, proven relays when adding random relays to publish.", "Session relays preset working": "Working preset relays", - "Session relays preset working hint": "Preset relays (from app defaults) that have not reached the session failure threshold (2 failures).", - "Session relays preset striked": "Striked preset relays", - "Session relays preset striked hint": "Preset relays that have reached 2 connection or publish failures this session and are skipped for reads and writes until you reload the app.", + "Session relays preset working hint": "Preset relays from app defaults.", "Session relays scored random": "Scored random relays", "Session relays scored random hint": "Relays that have accepted at least one publish this session; used to prefer faster relays when picking random relays. Sorted by average latency.", - "Session relays all striked": "All striked relays (any source)", - "Session relays clear strike": "Allow again", - "Session relays clear strike hint": "Remove this relay from the session block list; it will be used again until new connection failures.", - "relaySessionStrikes.bannerWarning": "This relay has {{count}} session strike(s) (limit {{threshold}}) after connection or query failures.", - "relaySessionStrikes.bannerSkipped": "This relay has reached the session failure limit ({{threshold}} strikes) and is skipped for reads and publishes in this tab.", - "relaySessionStrikes.refreshHint": "Use {{refresh}} to clear strikes for this relay and load the feed again.", successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 4257076d..c8db277f 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -513,7 +513,6 @@ export default { "Seen on": "Widziany na", "Active relays": "Active relays", "Not connected": "Not connected", - "Relay session striked": "Skipped this session (too many connection failures)", "More relays": "+{{count}} relays", "Temporarily display this reply": "Tymczasowo wyświetl tę odpowiedź", "Note not found": "Nie znaleziono wpisu", @@ -604,19 +603,11 @@ export default { relayType_contextual: "Reply/PM", relayType_randomly_selected: "Random (optional)", "Session relays": "Session relays", - "Session relays tab description": "Relay logic for this session: working and striked preset relays, and scored random relays (used to prefer faster, proven relays when adding random relays to publish).", + "Session relays tab description": "Relay logic for this session: preset relays and scored random relays used to prefer faster, proven relays when adding random relays to publish.", "Session relays preset working": "Working preset relays", - "Session relays preset working hint": "Preset relays (from app defaults) that have not reached 3 publish failures this session.", - "Session relays preset striked": "Striked preset relays", - "Session relays preset striked hint": "Preset relays that have reached 3 publish failures this session and are skipped for the rest of the session.", + "Session relays preset working hint": "Preset relays from app defaults.", "Session relays scored random": "Scored random relays", "Session relays scored random hint": "Relays that have accepted at least one publish this session; used to prefer faster relays when picking random relays. Sorted by average latency.", - "Session relays all striked": "All striked relays (any source)", - "Session relays clear strike": "Allow again", - "Session relays clear strike hint": "Remove this relay from the session block list; it will be used again until new connection failures.", - "relaySessionStrikes.bannerWarning": "This relay has {{count}} session strike(s) (limit {{threshold}}) after connection or query failures.", - "relaySessionStrikes.bannerSkipped": "This relay has reached the session failure limit ({{threshold}} strikes) and is skipped for reads and publishes in this tab.", - "relaySessionStrikes.refreshHint": "Use {{refresh}} to clear strikes for this relay and load the feed again.", successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index fe3c68e6..92d8179a 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -513,7 +513,6 @@ export default { "Seen on": "Просмотрено на", "Active relays": "Active relays", "Not connected": "Not connected", - "Relay session striked": "Skipped this session (too many connection failures)", "More relays": "+{{count}} relays", "Temporarily display this reply": "Временно отобразить этот ответ", "Note not found": "Заметка не найдена", @@ -604,19 +603,11 @@ export default { relayType_contextual: "Reply/PM", relayType_randomly_selected: "Random (optional)", "Session relays": "Session relays", - "Session relays tab description": "Relay logic for this session: working and striked preset relays, and scored random relays (used to prefer faster, proven relays when adding random relays to publish).", + "Session relays tab description": "Relay logic for this session: preset relays and scored random relays used to prefer faster, proven relays when adding random relays to publish.", "Session relays preset working": "Working preset relays", - "Session relays preset working hint": "Preset relays (from app defaults) that have not reached 3 publish failures this session.", - "Session relays preset striked": "Striked preset relays", - "Session relays preset striked hint": "Preset relays that have reached 3 publish failures this session and are skipped for the rest of the session.", + "Session relays preset working hint": "Preset relays from app defaults.", "Session relays scored random": "Scored random relays", "Session relays scored random hint": "Relays that have accepted at least one publish this session; used to prefer faster relays when picking random relays. Sorted by average latency.", - "Session relays all striked": "All striked relays (any source)", - "Session relays clear strike": "Allow again", - "Session relays clear strike hint": "Remove this relay from the session block list; it will be used again until new connection failures.", - "relaySessionStrikes.bannerWarning": "This relay has {{count}} session strike(s) (limit {{threshold}}) after connection or query failures.", - "relaySessionStrikes.bannerSkipped": "This relay has reached the session failure limit ({{threshold}} strikes) and is skipped for reads and publishes in this tab.", - "relaySessionStrikes.refreshHint": "Use {{refresh}} to clear strikes for this relay and load the feed again.", successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", diff --git a/src/i18n/locales/tr.ts b/src/i18n/locales/tr.ts index 6ba75768..eeb20647 100644 --- a/src/i18n/locales/tr.ts +++ b/src/i18n/locales/tr.ts @@ -513,7 +513,6 @@ export default { "Seen on": "Seen on", "Active relays": "Active relays", "Not connected": "Not connected", - "Relay session striked": "Skipped this session (too many connection failures)", "More relays": "+{{count}} relays", "Temporarily display this reply": "Temporarily display this reply", "Note not found": "Note not found", @@ -604,19 +603,11 @@ export default { relayType_contextual: "Reply/PM", relayType_randomly_selected: "Random (optional)", "Session relays": "Session relays", - "Session relays tab description": "Relay logic for this session: working and striked preset relays, and scored random relays (used to prefer faster, proven relays when adding random relays to publish). Striked relays are skipped for reads and publishes until reload.", + "Session relays tab description": "Relay logic for this session: preset relays and scored random relays used to prefer faster, proven relays when adding random relays to publish.", "Session relays preset working": "Working preset relays", - "Session relays preset working hint": "Preset relays (from app defaults) that have not reached the session failure threshold (2 failures).", - "Session relays preset striked": "Striked preset relays", - "Session relays preset striked hint": "Preset relays that have reached 2 connection or publish failures this session and are skipped for reads and writes until you reload the app.", + "Session relays preset working hint": "Preset relays from app defaults.", "Session relays scored random": "Scored random relays", "Session relays scored random hint": "Relays that have accepted at least one publish this session; used to prefer faster relays when picking random relays. Sorted by average latency.", - "Session relays all striked": "All striked relays (any source)", - "Session relays clear strike": "Allow again", - "Session relays clear strike hint": "Remove this relay from the session block list; it will be used again until new connection failures.", - "relaySessionStrikes.bannerWarning": "This relay has {{count}} session strike(s) (limit {{threshold}}) after connection or query failures.", - "relaySessionStrikes.bannerSkipped": "This relay has reached the session failure limit ({{threshold}} strikes) and is skipped for reads and publishes in this tab.", - "relaySessionStrikes.refreshHint": "Use {{refresh}} to clear strikes for this relay and load the feed again.", successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 6722c678..cb8884f4 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -513,7 +513,6 @@ export default { "Seen on": "来自", "Active relays": "Active relays", "Not connected": "Not connected", - "Relay session striked": "Skipped this session (too many connection failures)", "More relays": "+{{count}} relays", "Temporarily display this reply": "临时显示此回复", "Note not found": "未找到该笔记", @@ -604,19 +603,11 @@ export default { relayType_contextual: "Reply/PM", relayType_randomly_selected: "Random (optional)", "Session relays": "Session relays", - "Session relays tab description": "Relay logic for this session: working and striked preset relays, and scored random relays (used to prefer faster, proven relays when adding random relays to publish).", + "Session relays tab description": "Relay logic for this session: preset relays and scored random relays used to prefer faster, proven relays when adding random relays to publish.", "Session relays preset working": "Working preset relays", - "Session relays preset working hint": "Preset relays (from app defaults) that have not reached 3 publish failures this session.", - "Session relays preset striked": "Striked preset relays", - "Session relays preset striked hint": "Preset relays that have reached 3 publish failures this session and are skipped for the rest of the session.", + "Session relays preset working hint": "Preset relays from app defaults.", "Session relays scored random": "Scored random relays", "Session relays scored random hint": "Relays that have accepted at least one publish this session; used to prefer faster relays when picking random relays. Sorted by average latency.", - "Session relays all striked": "All striked relays (any source)", - "Session relays clear strike": "Allow again", - "Session relays clear strike hint": "Remove this relay from the session block list; it will be used again until new connection failures.", - "relaySessionStrikes.bannerWarning": "This relay has {{count}} session strike(s) (limit {{threshold}}) after connection or query failures.", - "relaySessionStrikes.bannerSkipped": "This relay has reached the session failure limit ({{threshold}} strikes) and is skipped for reads and publishes in this tab.", - "relaySessionStrikes.refreshHint": "Use {{refresh}} to clear strikes for this relay and load the feed again.", successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", diff --git a/src/lib/favorites-feed-relays.ts b/src/lib/favorites-feed-relays.ts index 15a94a67..a6545351 100644 --- a/src/lib/favorites-feed-relays.ts +++ b/src/lib/favorites-feed-relays.ts @@ -17,7 +17,6 @@ import { relayUrlsLocalsFirst } from '@/lib/relay-url-priority' import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-policy' -import { stripNostrLandAggrRelay } from '@/lib/nostr-land-aggr' import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize' const blockedSet = (blockedRelays: string[]) => @@ -51,7 +50,7 @@ export function getFavoritesFeedRelayUrls( }) const base = visible.length > 0 ? visible : DEFAULT_FAVORITE_RELAYS return feedRelayPolicyUrls( - [{ source: 'favorites', urls: stripNostrLandAggrRelay(base) }], + [{ source: 'favorites', urls: base }], { operation: 'favorites-feed', blockedRelays, @@ -128,16 +127,6 @@ export type ReadRelayPriorityOptions = { * relays in `SOCIAL_KIND_BLOCKED_RELAY_URLS` before capping. */ applySocialKindBlockedFilter?: boolean - /** - * When false, ignore each subrequest’s `urls` and use only the shared prioritized stack (rare). - * Default true. - */ - mergeSubrequestRelayUrls?: boolean - /** - * When true, fold `r.urls` into the author-outbox tier only (no extra first layer). Use for GIF / explicit spell relays - * that should rank with author outboxes, not ahead of user inboxes. Default false: prepend `r.urls` before user tiers. - */ - mergeSubrequestRelaysIntoAuthorTier?: boolean } /** @@ -216,9 +205,7 @@ export function buildProfilePageReadRelayUrls( /** * Per subrequest: shared inbox → author/favorites → fast read stack, normalized, user-blocked and (when applicable) - * social-kind-blocked stripped, deduped, capped. Subrequest `urls` are prepended first by default (following shards); - * set {@link ReadRelayPriorityOptions.mergeSubrequestRelaysIntoAuthorTier} to fold them into the author tier only - * (e.g. curated GIF / spell relay lists). + * social-kind-blocked stripped, deduped, capped. Subrequest `urls` are prepended first so explicit shard hints win. */ export function augmentSubRequestsWithFavoritesFastReadAndInbox( requests: TFeedSubRequest[], @@ -234,8 +221,6 @@ export function augmentSubRequestsWithFavoritesFastReadAndInbox( if (n) userReadSocialExempt.add(n) } return requests.map((r) => { - const useSubUrls = options?.mergeSubrequestRelayUrls !== false - const foldIntoAuthor = options?.mergeSubrequestRelaysIntoAuthorTier === true const applySocial = options?.applySocialKindBlockedFilter !== undefined ? options.applySocialKindBlockedFilter @@ -243,37 +228,19 @@ export function augmentSubRequestsWithFavoritesFastReadAndInbox( const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) - if (!useSubUrls) { - return { - ...r, - urls: buildPrioritizedReadRelayUrls({ - userReadRelays: userInboxReadRelays, - userWriteRelays: options?.userWriteRelays ?? [], - authorWriteRelays: options?.authorWriteRelays ?? [], - favoriteRelays: favorites, - blockedRelays, - maxRelays: max, - applySocialKindBlockedFilter: applySocial - }) - } - } - const authorOnly = dedupeNormalizeRelayUrlsOrdered(options?.authorWriteRelays ?? []) - const authorTier = foldIntoAuthor - ? dedupeNormalizeRelayUrlsOrdered([...authorOnly, ...r.urls]) - : authorOnly const coreLayers = buildReadRelayPriorityLayers({ userReadRelays: userInboxReadRelays, userWriteRelays: options?.userWriteRelays ?? [], - authorWriteRelays: authorTier, + authorWriteRelays: authorOnly, favoriteRelays: favorites }) - const layers = foldIntoAuthor ? coreLayers : [relayUrlsLocalsFirst(r.urls), ...coreLayers] + const layers = [relayUrlsLocalsFirst(r.urls), ...coreLayers] const policyLayers: FeedRelayLayer[] = layers.map((urls, index) => ({ - source: index === 0 && !foldIntoAuthor ? 'explicit' : index === 0 ? 'viewer-read' : 'fallback', + source: index === 0 ? 'explicit' : index === 1 ? 'viewer-read' : 'fallback', urls })) return { diff --git a/src/lib/index-relay-http.ts b/src/lib/index-relay-http.ts index dbb84c14..68a6f153 100644 --- a/src/lib/index-relay-http.ts +++ b/src/lib/index-relay-http.ts @@ -175,9 +175,6 @@ function rawToVerifiedEvent(raw: Record): NEvent | null { /** * Query one HTTP index relay. Runs one POST per filter when given an array. - * When every filter attempt fails with an HTTP response or a non-transport error and no events are returned, - * {@link options.onHardFailure} runs once (session strike parity with WebSocket relays). Pure browser transport - * failures (e.g. CORS on direct `https://` index POST) do not call it. */ function devHttpIndexRelayBaseForFetch(baseUrl: string): string { const n = normalizeHttpRelayUrl(baseUrl) || baseUrl @@ -187,15 +184,13 @@ function devHttpIndexRelayBaseForFetch(baseUrl: string): string { export async function queryIndexRelay( baseUrl: string, filter: Filter | Filter[], - options?: { signal?: AbortSignal; onHardFailure?: () => void } + options?: { signal?: AbortSignal } ): Promise { const base = devHttpIndexRelayBaseForFetch(baseUrl) const endpoint = indexRelayFilterUrl(base) const filters = Array.isArray(filter) ? filter : [filter] const out: NEvent[] = [] const seen = new Set() - /** Only set when the server returned HTTP (!ok) or a non-transport exception — not browser-only CORS / “failed to fetch”. */ - let strikeWorthyHttpFailure = false for (const f of filters) { const body = nostrFilterToIndexRelayBody(filterForIndexRelay(f)) try { @@ -210,7 +205,6 @@ export async function queryIndexRelay( timeoutMs: 25_000 }) if (!res.ok) { - strikeWorthyHttpFailure = true if (isDevViteIndexRelayProxyPath(endpoint)) { let detail = '' try { @@ -251,19 +245,10 @@ export async function queryIndexRelay( if (isIndexRelayTransportFailure(e)) { handleFilterTransportFailure(endpoint, e) } else { - strikeWorthyHttpFailure = true warnIndexRelayHttpThrottled(endpoint, '[IndexRelayHttp] filter request error', { endpoint, error: e }) } } } - if (strikeWorthyHttpFailure && out.length === 0 && filters.length > 0) { - // In dev, transport failures on the Vite loopback proxy (relay unreachable / proxy not yet ready) - // should not record session strikes — the relay may be temporarily down or the dev server - // needs a restart. Only real application errors (4xx/5xx from a live relay) trigger strikes in dev. - if (!isDevViteIndexRelayProxyPath(endpoint)) { - options?.onHardFailure?.() - } - } return out } diff --git a/src/lib/live-activities.ts b/src/lib/live-activities.ts index 10ec7ee6..31c40ddd 100644 --- a/src/lib/live-activities.ts +++ b/src/lib/live-activities.ts @@ -36,18 +36,6 @@ export type LiveActivitiesFetchEventsFn = ( /** NIP-53 live streaming (30311), meeting space (30312), meeting (30313). */ export const LIVE_ACTIVITY_KINDS = [30311, 30312, 30313] as const -/** - * @deprecated Home embeds no longer consult the kind picker. Kept for callers that still want - * “is live activity in the user’s selected kinds?” (e.g. optional UI); prefer inlining that check. - */ -export function liveActivityKindsEnabledInPicker( - showKinds: readonly number[], - feedKindFilterBypass: boolean -): boolean { - if (feedKindFilterBypass) return true - return LIVE_ACTIVITY_KINDS.some((k) => showKinds.includes(k)) -} - /** * Stable NIP-33 address `kind:pubkey:d` for a live-activity replaceable event (carousel dedupe / user hide list). * Must match {@link parseLiveActivityEvent} `address` and {@link dedupeLatestForLiveTicker} keys exactly diff --git a/src/lib/nostr-land-aggr.ts b/src/lib/nostr-land-aggr.ts index 8234dfaa..193b5fdf 100644 --- a/src/lib/nostr-land-aggr.ts +++ b/src/lib/nostr-land-aggr.ts @@ -1,93 +1,2 @@ -import type { TRelayList } from '@/types' -import { normalizeAnyRelayUrl } from '@/lib/url' - -/** Paid / subscription relay — presence in favorites or NIP-65 lists implies access to the matching aggregator. */ -export const NOSTR_LAND_WSS = 'wss://nostr.land' - -/** Aggregator for nostr.land subscribers only; others get auth / policy errors if contacted. */ +/** nostr.land aggregator used by read relay policy. */ export const AGGR_NOSTR_LAND_WSS = 'wss://aggr.nostr.land' - -function canonWs(url: string): string { - return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase() -} - -const NOSTR_LAND_CANON = canonWs(NOSTR_LAND_WSS) -const AGGR_CANON = canonWs(AGGR_NOSTR_LAND_WSS) - -/** True if this URL is the nostr.land websocket (normalized). */ -export function isNostrLandWsUrl(url: string | undefined | null): boolean { - if (!url?.trim()) return false - return canonWs(url) === NOSTR_LAND_CANON -} - -/** True if this URL is the nostr.land aggregator websocket (normalized). */ -export function isAggrNostrLandWsUrl(url: string | undefined | null): boolean { - if (!url?.trim()) return false - return canonWs(url) === AGGR_CANON -} - -/** True if any normalized URL equals nostr.land. */ -export function relayUrlListMentionsNostrLand(urls: readonly string[] | undefined): boolean { - if (!urls?.length) return false - for (const u of urls) { - if (isNostrLandWsUrl(u)) return true - } - return false -} - -/** True if nostr.land appears in any NIP-65 / HTTP relay list slice. */ -export function nip65RelayListMentionsNostrLand(rl: TRelayList | null | undefined): boolean { - if (!rl) return false - return ( - relayUrlListMentionsNostrLand(rl.read) || - relayUrlListMentionsNostrLand(rl.write) || - relayUrlListMentionsNostrLand(rl.httpRead) || - relayUrlListMentionsNostrLand(rl.httpWrite) - ) -} - -/** - * Subscriber may use {@link AGGR_NOSTR_LAND_WSS}: they listed nostr.land in kind-10012 favorites or NIP-65 lists. - */ -export function viewerMayUseNostrLandAggr( - favoriteRelays: readonly string[] | undefined, - nip65: TRelayList | null | undefined -): boolean { - if (relayUrlListMentionsNostrLand(favoriteRelays)) return true - return nip65RelayListMentionsNostrLand(nip65 ?? undefined) -} - -/** - * Drop {@link AGGR_NOSTR_LAND_WSS} when the viewer is not a nostr.land subscriber; otherwise ensure it appears once. - */ -export function applyNostrLandAggrRelayPolicy(urls: readonly string[], allowAggr: boolean): string[] { - const out: string[] = [] - const seen = new Set() - const push = (u: string) => { - const c = canonWs(u) - if (!c || seen.has(c)) return - if (!allowAggr && c === AGGR_CANON) return - seen.add(c) - out.push(normalizeAnyRelayUrl(u) || u.trim()) - } - for (const u of urls) { - push(u) - } - if (allowAggr && !seen.has(AGGR_CANON)) { - out.unshift(AGGR_NOSTR_LAND_WSS) - } - return out -} - -/** Remove the aggregator from relay stacks that must stay strictly user-curated (favorites feed). */ -export function stripNostrLandAggrRelay(urls: readonly string[]): string[] { - const out: string[] = [] - const seen = new Set() - for (const u of urls) { - const c = canonWs(u) - if (!c || c === AGGR_CANON || seen.has(c)) continue - seen.add(c) - out.push(normalizeAnyRelayUrl(u) || u.trim()) - } - return out -} diff --git a/src/lib/rss-article.ts b/src/lib/rss-article.ts index 9efca4df..57cee67d 100644 --- a/src/lib/rss-article.ts +++ b/src/lib/rss-article.ts @@ -144,14 +144,6 @@ export function expandArticleUrlThreadQueryValues(canonicalUrl: string): string[ return [...out] } -/** - * Values for a REQ `#r` filter on kind 9802 / kind 7 when the thread key is a canonical article URL. - * @deprecated Prefer {@link expandArticleUrlThreadQueryValues} — same values. - */ -export function computeRTagFilterValuesForArticleThread(canonicalUrl: string): string[] { - return expandArticleUrlThreadQueryValues(canonicalUrl) -} - /** True if `urlFromEvent` refers to the same article as `canonicalThreadKey` (after normalization + variant match). */ export function articleUrlMatchesThreadScope(urlFromEvent: string, canonicalThreadKey: string): boolean { const key = canonicalizeRssArticleUrl(canonicalThreadKey) diff --git a/src/lib/url.ts b/src/lib/url.ts index 47e9ceab..a38e00f5 100644 --- a/src/lib/url.ts +++ b/src/lib/url.ts @@ -82,10 +82,10 @@ export function normalizeAnyRelayUrl(url: string): string { } /** - * Stable key for per-relay session counters (strikes, publish stats): HTTP NIP-86 bases map to the same host’s - * `wss://…` URL so `https://nos.lol` and `wss://nos.lol` share one bucket (fixes preset vs “all striked” mismatch). + * Stable key for per-relay session stats: HTTP NIP-86 bases map to the same host’s + * `wss://…` URL so `https://nos.lol` and `wss://nos.lol` share one bucket. */ -export function canonicalRelayStrikeKey(url: string): string { +export function canonicalRelaySessionKey(url: string): string { const stepped = (normalizeAnyRelayUrl(url) || url.trim()).trim() if (!stepped) return '' if (isHttpRelayUrl(stepped)) { diff --git a/src/pages/primary/RelayPage/index.tsx b/src/pages/primary/RelayPage/index.tsx index 70f57ef1..aceb56e2 100644 --- a/src/pages/primary/RelayPage/index.tsx +++ b/src/pages/primary/RelayPage/index.tsx @@ -4,7 +4,6 @@ import Relay from '@/components/Relay' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import { TPageRef } from '@/types' import { normalizeAnyRelayUrl, simplifyUrl } from '@/lib/url' -import client from '@/services/client.service' import { Server } from 'lucide-react' import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react' @@ -14,9 +13,8 @@ const RelayPage = forwardRef(({ url }, ref) => { const feedRef = useRef(null) const runRefresh = useCallback(() => { - if (normalizedUrl) client.clearSessionRelayStrikeForUrl(normalizedUrl) feedRef.current?.refresh() - }, [normalizedUrl]) + }, []) useImperativeHandle( ref, diff --git a/src/pages/secondary/RelayPage/index.tsx b/src/pages/secondary/RelayPage/index.tsx index a9058758..9c4a7db6 100644 --- a/src/pages/secondary/RelayPage/index.tsx +++ b/src/pages/secondary/RelayPage/index.tsx @@ -4,7 +4,6 @@ import { RefreshButton } from '@/components/RefreshButton' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' import { normalizeAnyRelayUrl, simplifyUrl } from '@/lib/url' -import client from '@/services/client.service' import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react' import NotFoundPage from '../NotFoundPage' @@ -15,9 +14,8 @@ const RelayPage = forwardRef(({ url, index, hideTitlebar = false }: { url?: stri const title = useMemo(() => (url ? simplifyUrl(url) : undefined), [url]) const bumpFeed = useCallback(() => { - if (normalizedUrl) client.clearSessionRelayStrikeForUrl(normalizedUrl) feedRef.current?.refresh() - }, [normalizedUrl]) + }, []) useEffect(() => { if (!hideTitlebar) { diff --git a/src/services/client-query.service.ts b/src/services/client-query.service.ts index 9ad1ee77..515eb0ac 100644 --- a/src/services/client-query.service.ts +++ b/src/services/client-query.service.ts @@ -28,7 +28,7 @@ import { isIndexRelayTransportFailure, queryIndexRelay } from '@/lib/index-relay import logger from '@/lib/logger' import { isHttpRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url' import { RelaySubscribeOpBatch } from '@/services/relay-operation-log.service' -import { patchRelayNoticeForFetchFailures } from '@/services/relay-notice-strike' +import { patchRelayNoticeForFetchFailures } from '@/services/relay-notice-fetch-failure' import type { Filter, Event as NEvent } from 'nostr-tools' import { SimplePool, EventTemplate, VerifiedEvent, nip19 } from 'nostr-tools' import type { AbstractRelay } from 'nostr-tools/abstract-relay' @@ -160,23 +160,17 @@ export interface SubscribeCallbacks { } export type QueryServiceRelaySessionOptions = { - /** Skip opening REQ/publish paths to this normalized URL for the rest of the page session. */ - shouldSkipRelayForSession?: (normalizedUrl: string) => boolean - /** After failed `ensureRelay` (timeout / connection error), increment client session strike counter. */ - onRelayConnectionFailure?: (normalizedUrl: string) => void - /** NOTICE "failed to fetch events" (and similar) → same strike treatment as connection failure. */ - onRelayNoticeStrike?: (normalizedUrl: string, noticeMessage: string) => void + /** NOTICE "failed to fetch events" and similar backend failures. */ + onRelayNoticeFetchFailure?: (normalizedUrl: string, noticeMessage: string) => void } export class QueryService { private pool: SimplePool private signer?: ISigner private signerType?: TSignerType - private shouldSkipRelayForSession?: (normalizedUrl: string) => boolean - private onRelayConnectionFailure?: (normalizedUrl: string) => void /** Optional: ingest every resolved `query()` result (e.g. session event LRU). */ private onQueryResultIngest?: (events: NEvent[]) => void - private onRelayNoticeStrike?: (normalizedUrl: string, noticeMessage: string) => void + private onRelayNoticeFetchFailure?: (normalizedUrl: string, noticeMessage: string) => void /** Max concurrent REQ subscriptions per relay URL (see {@link MAX_CONCURRENT_SUBS_PER_RELAY}). */ private static readonly SUB_SLOT_CAP_PER_RELAY = MAX_CONCURRENT_SUBS_PER_RELAY @@ -209,9 +203,7 @@ export class QueryService { constructor(pool: SimplePool, relaySession?: QueryServiceRelaySessionOptions) { this.pool = pool - this.shouldSkipRelayForSession = relaySession?.shouldSkipRelayForSession - this.onRelayConnectionFailure = relaySession?.onRelayConnectionFailure - this.onRelayNoticeStrike = relaySession?.onRelayNoticeStrike + this.onRelayNoticeFetchFailure = relaySession?.onRelayNoticeFetchFailure } /** Wire after {@link EventService} exists: each `query()` / `fetchEvents` event is ingested from `onevent` (session LRU). */ @@ -354,7 +346,7 @@ export class QueryService { .map((u) => normalizeHttpRelayUrl(u) || u) .filter(Boolean) ) - ).filter((base) => !this.shouldSkipRelayForSession?.(base)) + ) const wsQueryUrls = urls.filter((u) => !isHttpRelayUrl(u)) return await new Promise((resolve) => { @@ -377,10 +369,7 @@ export class QueryService { : Promise.allSettled( httpRelayBases.map(async (base) => { try { - const evts = await queryIndexRelay(base, effectiveFilter, { - signal: abortHttp.signal, - onHardFailure: () => this.onRelayConnectionFailure?.(base) - }) + const evts = await queryIndexRelay(base, effectiveFilter, { signal: abortHttp.signal }) for (const evt of evts) { if (resolved) return eventCount++ @@ -611,13 +600,6 @@ export class QueryService { relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS]) } } - if (this.shouldSkipRelayForSession) { - relays = relays.filter((url) => { - const n = normalizeUrl(url) || url - return !this.shouldSkipRelayForSession!(n) - }) - } - relays = relays.filter((url) => !isHttpRelayUrl(url)) if (relays.length === 0) { @@ -702,9 +684,8 @@ export class QueryService { relay = await this.pool.ensureRelay(url, { connectionTimeout: RELAY_POOL_CONNECTION_TIMEOUT_MS }) - patchRelayNoticeForFetchFailures(relay, relayKey, this.onRelayNoticeStrike) + patchRelayNoticeForFetchFailures(relay, relayKey, this.onRelayNoticeFetchFailure) } catch (err) { - this.onRelayConnectionFailure?.(relayKey) this.releaseSubSlot(relayKey) handleClose(i, (err as Error)?.message ?? String(err)) return @@ -749,10 +730,9 @@ export class QueryService { liveRelay = await this.pool.ensureRelay(url, { connectionTimeout: RELAY_POOL_CONNECTION_TIMEOUT_MS }) - patchRelayNoticeForFetchFailures(liveRelay, relayKey, this.onRelayNoticeStrike) + patchRelayNoticeForFetchFailures(liveRelay, relayKey, this.onRelayNoticeFetchFailure) } catch (err) { nip42ResubscribePending.delete(i) - this.onRelayConnectionFailure?.(relayKey) this.releaseSubSlot(relayKey) handleClose(i, (err as Error)?.message ?? String(err)) return diff --git a/src/services/client.service.ts b/src/services/client.service.ts index ed2e09bb..02a9af93 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -129,7 +129,7 @@ import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize' import { - canonicalRelayStrikeKey, + canonicalRelaySessionKey, isHttpRelayUrl, isLocalNetworkUrl, normalizeAnyRelayUrl, @@ -170,7 +170,7 @@ import indexedDb from './indexed-db.service' import { invalidateArchiveFootprintCache } from './event-archive.service' import { notifyLiveActivitiesPrewarmComplete } from './live-activities-prewarm-bridge' import nip66Service from './nip66.service' -import { patchRelayNoticeForFetchFailures } from '@/services/relay-notice-strike' +import { patchRelayNoticeForFetchFailures } from '@/services/relay-notice-fetch-failure' import { compactFilterForRelayLog, RelayOpTerminalRow, @@ -182,9 +182,6 @@ import { EventService } from './client-events.service' import { ReplaceableEventService } from './client-replaceable-events.service' import { MacroService, createBookstrService } from './client-macro.service' -/** Fired on `window` when session relay strike counts change (subscribe in single-relay UI). */ -export const JUMBLE_SESSION_RELAY_STRIKES_CHANGED = 'jumble:session-relay-strikes-changed' as const - /** Live timeline REQ: EOSE caps “connected but silent” relays. */ const SUBSCRIBE_RELAY_EOSE_TIMEOUT_MS = 4800 /** Coalesce pre-EOSE timeline snapshots; `setTimeout` so updates still run when rAF is throttled (background tab). */ @@ -334,7 +331,7 @@ class ClientService extends EventTarget { // Initialize sub-services this.queryService = new QueryService(this.pool, { - onRelayNoticeStrike: (normalizedUrl, noticeMessage) => + onRelayNoticeFetchFailure: (normalizedUrl, noticeMessage) => this.logRelayNoticeFetchFailure(normalizedUrl, noticeMessage) }) this.eventService = new EventService(this.queryService) @@ -1138,45 +1135,16 @@ class ClientService extends EventTarget { /** NOTICE "failed to fetch events" — logged only (no session relay blocking). */ private logRelayNoticeFetchFailure(url: string, noticeMessage: string) { - const n = canonicalRelayStrikeKey(url) + const n = canonicalRelaySessionKey(url) logger.debug('[Relay] NOTICE failed-fetch', { url: n ?? url, noticeSnippet: noticeMessage.slice(0, 220) }) } - /** Legacy API: session strikes removed; always zero. */ - getSessionRelayStrikeCountForUrl(_url: string): number { - return 0 - } - - getSessionRelayFailureStrikeThreshold(): number { - return 4 - } - - /** Legacy API: session strikes removed; relays are never skipped for reads for flaky connections. */ - isSessionRelayStrikedForReads(_url: string): boolean { - return false - } - - /** No-op: use relay block list in settings instead of automatic session strikes. */ - clearSessionRelayStrikes(): void {} - - clearSessionRelayStrikeForUrl(_url: string): boolean { - return false - } - - clearSessionRelayStrikesForUrls(_urls: string[]): number { - return 0 - } - - private relayUrlsAfterStrikesOrRecover(urls: string[]): string[] { - return Array.from(new Set(urls)) - } - /** Record a successful publish and its latency for session-based preference when selecting random relays. */ recordPublishSuccess(url: string, latencyMs: number) { - const n = canonicalRelayStrikeKey(url) + const n = canonicalRelaySessionKey(url) if (!n) return const cur = this.sessionRelayPublishStats.get(n) if (cur) { @@ -1196,7 +1164,7 @@ class ClientService extends EventTarget { const out: string[] = [] for (const [url, stats] of this.sessionRelayPublishStats.entries()) { if (stats.successCount < 1) continue - const n = canonicalRelayStrikeKey(url) + const n = canonicalRelaySessionKey(url) if (!n || readOnlySet.has(n)) continue out.push(n) } @@ -1209,14 +1177,10 @@ class ClientService extends EventTarget { return out } - /** - * Session-only debug for Settings: scored publish relays (no automatic session strikes). - */ + /** Session-only debug for Settings: scored publish relays. */ getSessionRelayDebug(): { - strikedUrls: string[] scoredRelays: { url: string; successCount: number; avgLatencyMs: number }[] presetWorking: string[] - presetStriked: string[] } { const presetSet = new Set() for (const u of [ @@ -1226,7 +1190,7 @@ class ClientService extends EventTarget { ...SEARCHABLE_RELAY_URLS ]) { const n = normalizeUrl(u) || u - if (n) presetSet.add(canonicalRelayStrikeKey(n)) + if (n) presetSet.add(canonicalRelaySessionKey(n)) } const preset = Array.from(presetSet) const scoredRelays = Array.from(this.sessionRelayPublishStats.entries()).map(([url, s]) => ({ @@ -1235,7 +1199,7 @@ class ClientService extends EventTarget { avgLatencyMs: Math.round(s.sumLatencyMs / s.successCount) })) scoredRelays.sort((a, b) => a.avgLatencyMs - b.avgLatencyMs) - return { strikedUrls: [], scoredRelays, presetWorking: preset, presetStriked: [] } + return { scoredRelays, presetWorking: preset } } /** @@ -1251,14 +1215,14 @@ class ClientService extends EventTarget { const preferred: string[] = [] const rest: string[] = [] for (const url of unique) { - const sk = canonicalRelayStrikeKey(url) + const sk = canonicalRelaySessionKey(url) const stats = sk ? this.sessionRelayPublishStats.get(sk) : undefined if (stats && stats.successCount >= 1) preferred.push(url) else rest.push(url) } preferred.sort((a, b) => { - const sa = this.sessionRelayPublishStats.get(canonicalRelayStrikeKey(a)) - const sb = this.sessionRelayPublishStats.get(canonicalRelayStrikeKey(b)) + const sa = this.sessionRelayPublishStats.get(canonicalRelaySessionKey(a)) + const sb = this.sessionRelayPublishStats.get(canonicalRelaySessionKey(b)) if (!sa || !sb) return 0 if (sb.successCount !== sa.successCount) return sb.successCount - sa.successCount const avgA = sa.sumLatencyMs / sa.successCount @@ -1301,7 +1265,7 @@ class ClientService extends EventTarget { return true }) filtered = Array.from(new Set(filtered)) - filtered = this.relayUrlsAfterStrikesOrRecover(filtered) + filtered = Array.from(new Set(filtered)) const countAfterFiltersBeforeCap = filtered.length filtered = await this.capPublishRelayUrlsForPublish( filtered, @@ -1323,7 +1287,7 @@ class ClientService extends EventTarget { maxPublishRelays: MAX_PUBLISH_RELAYS, fromPickerOrDetermineCount: relayUrls.length, afterMergeWithYourOutboxes: mergedRelayUrls.length, - afterReadonlySocialAndStrikeFilter: countAfterFiltersBeforeCap, + afterReadonlySocialFilter: countAfterFiltersBeforeCap, finalContactedRelayCount: uniqueRelayUrls.length, finalRelays: uniqueRelayUrls, explain: @@ -2120,8 +2084,7 @@ class ClientService extends EventTarget { ) { const originalDedupedRelays = Array.from(new Set(urls)) let relays = originalDedupedRelays.filter((url) => !isHttpRelayUrl(url)) - // While offline, silently drop every non-local relay so nothing is added to - // groupedRequests and no session strike is recorded for a connectivity-induced failure. + // While offline, silently drop every non-local relay so nothing is added to groupedRequests. if (!navigator.onLine) { relays = relays.filter((url) => isLocalNetworkUrl(url)) } @@ -2156,7 +2119,7 @@ class ClientService extends EventTarget { relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS]) } } - relays = this.relayUrlsAfterStrikesOrRecover(relays) + relays = Array.from(new Set(relays)) // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this @@ -2971,9 +2934,9 @@ class ClientService extends EventTarget { const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url)) relays = relaysAfterSocialKindBlockedStrip(wsOriginal, stripped) } - relays = this.relayUrlsAfterStrikesOrRecover(relays) + relays = Array.from(new Set(relays)) let queryRelays = dedupeNormalizeRelayUrlsOrdered([...relays, ...httpRelayBases]) - /** If every candidate was session-striked / filtered away, still hit public read mirrors so REQ does not no-op. */ + /** If every candidate was filtered away, still hit public read mirrors so REQ does not no-op. */ if (queryRelays.length === 0) { queryRelays = dedupeNormalizeRelayUrlsOrdered([...FAST_READ_RELAY_URLS]) } diff --git a/src/services/mention-event-search.service.ts b/src/services/mention-event-search.service.ts index f27b2061..9e68c320 100644 --- a/src/services/mention-event-search.service.ts +++ b/src/services/mention-event-search.service.ts @@ -195,16 +195,6 @@ export async function searchCitationEventsForPicker( return searchEventsForPicker(query, limit, 'nevent', CITATION_PICKER_KINDS) } -/** - * @deprecated Use searchEventsForPicker(query, limit, 'nevent') instead. - */ -export async function searchNotesForPicker( - query: string, - limit: number = DEFAULT_NOTES_LIMIT -): Promise { - return searchEventsForPicker(query, limit, 'nevent') -} - /** * Search for npubs for @-mentions. Uses same pattern as note search: cache (follow + local index) then relays. * Delegates to client which already does follow-list → local index → relay search. diff --git a/src/services/note-stats.service.ts b/src/services/note-stats.service.ts index 4dd00cd2..186d6eef 100644 --- a/src/services/note-stats.service.ts +++ b/src/services/note-stats.service.ts @@ -480,7 +480,7 @@ class NoteStatsService { const add = (url: string | undefined) => { if (!url) return // Must use normalizeAnyRelayUrl, not normalizeUrl: the latter converts http(s):// - // index relay URLs into ws(s):// which then hit the WebSocket pool and get session strikes. + // index relay URLs into ws(s):// which then hit the WebSocket pool. const n = normalizeAnyRelayUrl(url) if (!n || blocked.has(n.toLowerCase()) || seen.has(n)) return seen.add(n) diff --git a/src/services/relay-notice-strike.ts b/src/services/relay-notice-fetch-failure.ts similarity index 69% rename from src/services/relay-notice-strike.ts rename to src/services/relay-notice-fetch-failure.ts index 67ce5464..4550c653 100644 --- a/src/services/relay-notice-strike.ts +++ b/src/services/relay-notice-fetch-failure.ts @@ -2,25 +2,25 @@ import type { AbstractRelay } from 'nostr-tools/abstract-relay' const patched = new WeakSet() -/** NOTICE bodies that indicate the relay backend failed to serve the REQ — count as a session strike. */ +/** NOTICE bodies that indicate the relay backend failed to serve the REQ. */ const FAILED_FETCH_EVENTS = /failed to fetch events/i /** - * One-time patch: relay NOTICE "failed to fetch events" → session strike (same as connection failure). + * One-time patch: relay NOTICE "failed to fetch events" -> diagnostic callback. * Safe to call on every ensureRelay; only the first patch per relay instance applies. */ export function patchRelayNoticeForFetchFailures( relay: AbstractRelay, relayKey: string, - onStrike?: (normalizedUrl: string, noticeMessage: string) => void + onFailure?: (normalizedUrl: string, noticeMessage: string) => void ): void { - if (!onStrike || patched.has(relay as object)) return + if (!onFailure || patched.has(relay as object)) return patched.add(relay as object) const previous = relay.onnotice.bind(relay) relay.onnotice = (msg: string) => { if (typeof msg === 'string' && FAILED_FETCH_EVENTS.test(msg)) { try { - onStrike(relayKey, msg) + onFailure(relayKey, msg) } catch { /* ignore */ }