Compare commits

...

53 Commits

Author SHA1 Message Date
Gigi
8c3baf1416 chore: bump version to 0.8.3 2025-10-20 09:29:11 +02:00
Gigi
e0c169edbc fix(highlights): avoid unintended reload by decoupling cached highlight sync from content loading in useExternalUrlLoader 2025-10-20 09:15:41 +02:00
Gigi
d2181ad772 fix(highlights): preserve immediate UI highlight after creation by merging streaming results instead of overwriting in article and external URL loaders 2025-10-20 09:07:42 +02:00
Gigi
8ff3f08d8c fix(highlights): restore FAB selection updates by listening to document selectionchange; keep clearing selection after creation 2025-10-20 08:57:00 +02:00
Gigi
e17e1bc824 fix(lint): resolve unused var and empty catch issues 2025-10-20 00:47:11 +02:00
Gigi
948674ae8c feat(reading-progress): stream mark-as-read reactions non-blockingly and emit updates as they arrive 2025-10-20 00:45:35 +02:00
Gigi
431f14f56d feat(reads): move highlighted filter next to All for prominence 2025-10-20 00:44:03 +02:00
Gigi
4cc9d557a0 feat(reads): add emoji filter, refine completed to 95%+, and show checkmark only at >=95% progress 2025-10-20 00:43:31 +02:00
Gigi
cc60f9584a temp: disable mark-as-read reactions loading due to queryEvents hanging
Temporarily skip loading mark-as-read reactions to unblock the reads feature.
Focus on getting reading progress working first.

TODO: Debug why queryEvents hangs when querying kind:7 and kind:17 reactions.
The Promise never resolves even though we're not using timeouts.
2025-10-20 00:38:14 +02:00
Gigi
94f1f9035b debug: add logging before/after queryEvents calls for reactions 2025-10-20 00:35:51 +02:00
Gigi
e5b1594933 feat: add listener for markedAsReadChanged events
Implemented event listener pattern in readingProgressController:
- Added onMarkedAsReadChanged() method for subscribers
- Added emitMarkedAsReadChanged() to notify when marked IDs update
- Call emitMarkedAsReadChanged() after loading reactions

In Me.tsx:
- Subscribe to onMarkedAsReadChanged() in new useEffect
- When fired, rebuild reads list with new marked-as-read items
- Include marked-only items (no progress event)

Now when reactions finish loading in background, /me/reads/completed
will update automatically with newly marked articles.
2025-10-20 00:34:38 +02:00
Gigi
2bf9b9789b debug: add detailed logging to mark-as-read reactions loading
Added comprehensive logging to see:
- When reactions queries start and complete
- How many kind:17 and kind:7 events are returned
- What reactions have MARK_AS_READ_EMOJI content
- Event ID to naddr mapping progress
- Final count of markedAsReadIds

This will help identify why markedAsReadIds is empty.
2025-10-20 00:33:01 +02:00
Gigi
d3405a4029 refactor: use bookmarkController pattern in readingProgressController
Non-blocking, background loading pattern:
- Subscribe to eventStore timeline immediately (returns right away)
- Mark as loaded immediately
- Fire-and-forget background queries for reading progress from relays
- Fire-and-forget background queries for mark-as-read reactions
- All updates stream via eventStore subscription

No timeouts. No blocking awaits. Updates arrive progressively as relays
respond, UI shows data as soon as eventStore delivers it.
2025-10-20 00:29:39 +02:00
Gigi
763f7bef4d debug: add granular logging to identify where loading hangs
Added logs at each step:
- Setting up timeline subscription
- Timeline subscription ready
- Querying reading progress events
- Got reading progress events count
- Generation changed abort

This will show exactly which step is blocking.
2025-10-20 00:23:04 +02:00
Gigi
e8e629f4e1 fix: prevent concurrent start() calls in readingProgressController
Added isLoading flag to block multiple start() calls from running in parallel.
The repeated start() calls were all waiting on queryEvents() calls,
creating a thundering herd that prevented any from completing.

Now only one start() runs at a time, and concurrent calls are skipped
with a console log.
2025-10-20 00:18:23 +02:00
Gigi
a0829e834f feat: mirror debug behavior in Me tabs for MARK_AS_READ
- Reads (/me/reads/completed): fetch kind:7 📚 reactions and map #e -> 30023 naddr; include as completed reads
- Links (/me/links/completed): fetch kind:17 📚 reactions and use #r URL; include as completed links
- Keep progress-based items from readingProgressController, but explicitly add marked-only items per tab

This matches the debug page behavior and splits articles vs links cleanly.
2025-10-20 00:00:00 +02:00
Gigi
ff938aa384 feat(reads): include marked-as-read-only items in /me/reads
If an article or URL is marked as read (📚) but has no reading
progress event yet, include it in the reads list so the 'completed'
filter surfaces it.

Uses readingProgressController.getMarkedAsReadIds() to synthesize
ReadItems for marked-only entries.
2025-10-19 23:57:20 +02:00
Gigi
3991bfeeb2 fix: move lastLoadedPubkey assignment to end of start() method
The bug: start() was setting lastLoadedPubkey at the beginning, so if
start() got called twice (which it was), the second call would see
isLoadedFor(pubkey) return true and skip the entire loading process,
including fetching mark-as-read reactions.

Fix: Only set lastLoadedPubkey AFTER all fetching is complete. This
ensures that concurrent start() calls don't skip the loading.

This allows kind:7 and kind:17 mark-as-read reactions to be fetched
and tracked properly.
2025-10-19 23:54:44 +02:00
Gigi
e8c35c8914 debug: add module-level log to confirm module is loaded
If this log doesn't appear in console, the module isn't being imported at all.
2025-10-19 23:53:31 +02:00
Gigi
46345c154b debug: add log before fetching mark-as-read reactions
Shows we reached the point where we're about to fetch kind:7/kind:17 reactions.
2025-10-19 23:53:00 +02:00
Gigi
f43dae92aa debug: add start() method logs to confirm controller initialization
Added initial logs to show:
- When start() is called
- Whether already loaded (and skipped)

This helps confirm the controller is even being initialized.
2025-10-19 23:52:24 +02:00
Gigi
99c164a5e9 debug: add detailed logging to understand markedAsReadIds population
Added:
- getMarkedAsReadIds() method to expose markedAsReadIds for debugging
- Final state logging showing all progressMap keys and markedAsReadIds
- Comprehensive logging throughout kind:7/kind:17 processing

This will help identify why markedAsRead articles aren't showing in /me/reads/completed.
Check console logs to see:
1. All progressMap entries (nadrs)
2. All markedAsReadIds entries
3. Step-by-step kind:7 and kind:17 processing
2025-10-19 23:51:05 +02:00
Gigi
569b4357f2 fix: skip title fetching for raw event IDs in HighlightCitation
The eventReference can be either:
1. Raw event ID (hex string) - from event pointers
2. Coordinate string (kind:pubkey:identifier) - from address pointers
3. Already-encoded naddr - from some sources

Raw event IDs cannot be converted to nadrs without additional context
(we don't have the kind, pubkey, or identifier), so skip title fetching
for them to avoid bech32 decoding errors.

Fixes console errors:
- 'Invalid checksum in <hex>'
- 'Unknown letter: "b". Allowed: qpzry9x8gf2tvdw0s3jn54khce6mua7l'

These errors occurred when trying to decode raw hex event IDs as bech32.
2025-10-19 23:49:12 +02:00
Gigi
de287c625b chore: remove relay.current.fyi from relay list
Removed 'wss://relay.current.fyi' from both api/article-og.ts and
src/config/relays.ts as this relay is no longer used.
2025-10-19 23:47:33 +02:00
Gigi
1424f6ebc5 debug: add console.log statements to debug mark-as-read reaction tracking
Added detailed logging throughout the kind:7 and kind:17 reaction
processing to understand:
- What reactions are being fetched
- Which ones have MARK_AS_READ_EMOJI
- Event ID extraction
- Article lookups
- Event ID to naddr mapping
- Final markedAsReadIds set

Check browser console when loading /me/reads to see the full flow.
2025-10-19 23:46:25 +02:00
Gigi
b0a368fc64 fix: properly handle kind:7 mark-as-read reactions with event ID to naddr mapping
Restored kind:7 reaction handling with proper implementation:
1. Fetch kind:7 reactions with MARK_AS_READ_EMOJI
2. Extract event IDs from #e tags
3. Fetch the referenced articles (kind:30023)
4. Build mapping of event IDs to nadrs
5. Add marked articles to markedAsReadIds using their nadrs

Now both kind:7 (Nostr articles) and kind:17 (URLs) mark-as-read
reactions are properly tracked and will appear in /me/reads/completed.

Added nip19 import for naddr encoding.
2025-10-19 23:44:56 +02:00
Gigi
6f8cf641b7 fix: correctly track mark-as-read reactions in readingProgressController
Fixed several issues:
1. Clear markedAsReadIds on reset() so it doesn't persist across logouts
2. Skip kind:7 reactions (events) as they require complex event ID to naddr mapping
3. Only process kind:17 reactions (URLs) which directly use URLs as identifiers
4. Correctly extract URL from #r tag instead of using emoji content

Now kind:17 mark-as-read reactions for external URLs are properly tracked.
These articles will appear in /me/reads/completed.
2025-10-19 23:42:22 +02:00
Gigi
23b4c3475f feat: track mark-as-read reactions in readingProgressController
Extended readingProgressController to also fetch and track mark-as-read
reactions (kind:7 and kind:17 with MARK_AS_READ_EMOJI) alongside reading
progress events.

Changes:
- Added markedAsReadIds Set to controller
- Query mark-as-read reactions in parallel with reading progress
- Added isMarkedAsRead() method to check if article is marked as read
- Updated Me.tsx to include markedAsRead status in ReadItems

Now /me/reads/completed shows:
- Articles with >= 95% reading progress
- Articles marked as read with the 📚 emoji
2025-10-19 23:33:22 +02:00
Gigi
5633dc640c refactor: simplify reads - use readingProgressController directly
Removed the complex readsController wrapper. Now /me/reads simply:
1. Uses readingProgressController (already loaded in App.tsx)
2. Converts progress map to ReadItems
3. Subscribes to progress updates

This is much simpler and DRY - no need for a separate controller.
Reading progress is already deduped and managed centrally.

Same approach as debug page - just use the data source directly.
2025-10-19 23:29:06 +02:00
Gigi
0f1dfa445a refactor: simplify reads loading - don't require bookmarks
Reads don't actually need bookmarks to load. Reading progress (kind:39802)
is independent and stands on its own. Bookmarks are just optional enrichment.

Changed:
- readsController.start() no longer takes bookmarks parameter
- Pass empty array to fetchAllReads instead
- Load reads immediately in App.tsx like highlights/writings
- No more circular dependency on bookmarks loading first

This is simpler and loads reading progress faster.
2025-10-19 23:26:00 +02:00
Gigi
ab5225de50 fix: emit all reading items not just articles
The onItem callback was filtering to only 'article' type items,
which excluded external URLs from reading progress. Now all items
(articles and external URLs) are emitted to readsController.

This fixes the empty reads list issue where reading progress exists
but wasn't being displayed.
2025-10-19 23:24:58 +02:00
Gigi
b89705cf43 feat: load reads centrally in App.tsx like bookmarks and highlights
- Import readsController in App.tsx
- Start readsController in the central useEffect when user logs in
- Pass bookmarks to readsController.start() for article lookups
- Simplify Me.tsx loadReadsTab to just mark tab as loaded
- Subscription to readsController in Me.tsx still streams updates to UI

This means:
- Reads load in the background automatically
- Data is available even before clicking the Reads tab
- Consistent with how bookmarks, highlights, and writings are loaded
- Non-blocking - readsController streams updates progressively
2025-10-19 23:23:16 +02:00
Gigi
740dd53299 fix: properly subscribe to readsController updates with useEffect
The loadReadsTab async function was trying to return cleanup functions,
which doesn't work in React. Moved the subscription logic to a separate
useEffect hook with empty dependency array so:
- Subscriptions are set up once on mount
- Cleanup happens properly on unmount
- readsController updates flow through to UI correctly

This fixes the empty reads list issue.
2025-10-19 23:21:12 +02:00
Gigi
eb61553c20 feat: create readsController following highlightsController pattern
- New src/services/readsController.ts manages all reading activity centrally
- Streams reading items as they arrive (progress, marks as read, bookmarks)
- Supports subscriptions via onReads() and onLoading() callbacks
- Tracks loading state and last synced timestamp per user
- Generation-based cancellation for logout/pubkey changes
- Deduplicates by article ID and sorts by reading activity
- Updated Me.tsx loadReadsTab to use readsController instead of calling fetchAllReads
- Provides same reactive, non-blocking UX as highlightsController
2025-10-19 23:19:46 +02:00
Gigi
8b708535ca fix: don't block UI while loading reads - stream updates as data arrives
Changed loadReadsTab to not await fetchAllReads. Instead:
- Start with empty state immediately
- Use onItem callback to stream updates as they're fetched
- Reading data flows in as it arrives (reading progress, marks as read, etc)
- UI doesn't block waiting for all article data to be fetched

Same pattern as debug page - provides responsive UI with progressive loading.
2025-10-19 23:17:40 +02:00
Gigi
f77761c002 feat: show all reading activity in /me/reads, not just bookmarks
Changed loadReadsTab to use fetchAllReads directly instead of deriveReadsFromBookmarks.
Now /me/reads shows ALL articles with any reading activity:
- Articles with reading progress (kind:39802)
- Articles marked as read (kind:7, kind:17 reactions)
- Articles with highlights
- Bookmarked articles

Previously only showed bookmarked articles and tried to enrich with reading data.
Now the reading data (progress, marks as read) is the primary source.
2025-10-19 23:15:53 +02:00
Gigi
b900666eb8 feat: add category breakdown to reading progress debug output
Shows counts of articles in each reading progress category:
- Unopened (0%)
- Started (0% < progress ≤ 10%)
- Reading (10% < progress ≤ 94%) - highlighted in green
- Completed (≥ 95%)

This helps understand why /me/reads/reading shows fewer articles than
the total reading progress events - most articles fall into other categories.
2025-10-19 23:11:38 +02:00
Gigi
2639c78957 feat: display both raw and deduplicated reading progress events
- Load raw events from queryEvents for transparency
- Load deduplicated results from readingProgressController in parallel
- Display raw events first, then deduplicated results below for comparison
- Helps debugging by showing all events plus the final processed state
2025-10-19 23:08:31 +02:00
Gigi
8320911bc9 refactor: use readingProgressController for deduplicated progress in debug
- Replace raw queryEvents with readingProgressController.start() for reading progress
- Controller already handles deduplication by article (d-tag) and keeps most recent
- Display deduplicated progress map below raw events for easy comparison
- Add progress percentage and visual progress bar for each article
- Add styling with blue background to distinguish deduplicated results
2025-10-19 23:07:32 +02:00
Gigi
00d6bd4c46 feat: add reading progress loading section to debug page
- Add state variables for reading progress events and mark-as-read reactions
- Implement handler to load all reading progress events (kind:39802) for logged-in user
- Implement handler to load all mark-as-read reactions (kind:7, kind:17) with MARK_AS_READ_EMOJI filter
- Add two new sections to debug page with buttons and results display
- Display event details including author, creation time, and relevant tags
- Include timing metrics for load operations
2025-10-19 23:02:15 +02:00
Gigi
cd377b6f26 docs: update CHANGELOG.md for v0.8.2 release
- Added reading progress indicator in compact cards
- Compact cards layout optimizations (reduced padding, row height, gaps)
- Reading progress bar styling (thinner, aligned with text)
- Fixed: Removed borders from compact bookmarks
2025-10-19 22:55:39 +02:00
Gigi
84b0339505 chore: bump version to 0.8.2 2025-10-19 22:54:46 +02:00
Gigi
12fa1db0db style: adjust progress bar margin in compact cards
- Reduce left margin from 1.75rem to 1.5rem for better visual balance
2025-10-19 22:54:19 +02:00
Gigi
0919091f19 style: align reading progress bar with text in compact cards
- Add left margin of 1.75rem to progress bar to start where text begins
- Prevents progress bar from looking like a separator
- Creates visual association between progress indicator and the specific bookmark item
2025-10-19 22:53:49 +02:00
Gigi
e1c04b4e7f fix: align progress bar to start at title position
- Add padding-left to progress bar container to offset it to title position
- Remove margin from inner fill
- Progress bar now visually starts where the title starts, not at the icon
2025-10-19 22:52:32 +02:00
Gigi
b9642067a1 fix: use margin instead of padding for reading progress bar alignment
- Move left offset from outer container padding to inner progress fill margin
- Background bar now spans full width while progress fill starts at text position
- Creates cleaner visual alignment without distorting the bar appearance
2025-10-19 22:51:36 +02:00
Gigi
ceca37df08 style: align reading progress bar with title text in compact cards
- Add left padding (1.85rem) to progress bar to align with bookmark title
- Progress bar now starts at the same position as the text content
2025-10-19 22:50:48 +02:00
Gigi
dfdc5d0946 style: make reading progress bar thinner in compact cards
- Reduce reading progress bar height from 2px to 1px
- Creates a more subtle, minimal progress indicator for compact bookmarks
2025-10-19 22:49:46 +02:00
Gigi
3619cd2585 fix: remove borders from compact bookmarks in sidebar
- Add explicit CSS rule to remove border from compact bookmarks in .bookmarks-list
- Override the border styling from me.css that was applying to all .individual-bookmark elements
- Ensure compact cards remain borderless and transparent
2025-10-19 22:49:01 +02:00
Gigi
f93e52611e style: make compact cards even more compact
- Reduce padding from 0.5rem to 0.25rem vertically
- Reduce compact row height from 28px to 24px
- Reduce gap between compact cards from 0.5rem to 0.25rem
- Creates a tighter, more space-efficient list layout
2025-10-19 22:48:17 +02:00
Gigi
ecb81cb151 feat: show reading progress in compact cards in bookmarks sidebar
- Add reading progress state and subscription to BookmarkList component
- Create helper function to get reading progress for both articles (using naddr) and web bookmarks (using URL)
- Update CompactView to display reading progress indicator for all bookmark types
- Progress indicator now shows for any bookmark with reading data, not just articles
2025-10-19 22:46:34 +02:00
Gigi
adf73cb9d1 fix: resolve all linting and type errors
- Fix empty catch blocks by adding explanatory comments
- Remove unused variables or prefix with underscore
- Remove orphaned object literals from removed console.log statements
- Fix unnecessary dependency array entries
- Ensure all empty code blocks have comments to satisfy eslint no-empty rule
2025-10-19 22:41:35 +02:00
Gigi
4202807777 refactor: remove all console.log debug output 2025-10-19 22:35:45 +02:00
67 changed files with 851 additions and 639 deletions

View File

@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.8.2] - 2025-10-19
### Added
- Reading progress indicator in compact bookmark cards
- Shows progress bar for articles and web bookmarks with reading data
- Progress bar aligned with bookmark text for better visual association
- Color-coded progress (blue for reading, green for completed, gray for started)
### Changed
- Compact cards layout optimizations for more space-efficient display
- Reduced vertical padding from 0.5rem to 0.25rem
- Reduced compact row height from 28px to 24px
- Reduced gap between compact cards from 0.5rem to 0.25rem
- Reading progress bar styling for compact view
- Bar height reduced from 2px to 1px for more subtle appearance
- Left margin of 1.5rem aligns bar with bookmark text instead of appearing as separator
### Fixed
- Removed borders from compact bookmarks in bookmarks sidebar
- Border styling from `.bookmarks-list` no longer applies to compact cards
- Compact cards now display as truly borderless and transparent
## [0.8.0] - 2025-10-19
### Added
@@ -2044,7 +2069,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Optimize relay usage following applesauce-relay best practices
- Use applesauce-react event models for better profile handling
[Unreleased]: https://github.com/dergigi/boris/compare/v0.8.0...HEAD
[Unreleased]: https://github.com/dergigi/boris/compare/v0.8.2...HEAD
[0.8.2]: https://github.com/dergigi/boris/compare/v0.8.0...v0.8.2
[0.8.0]: https://github.com/dergigi/boris/compare/v0.7.4...v0.8.0
[0.7.4]: https://github.com/dergigi/boris/compare/v0.7.3...v0.7.4
[0.7.3]: https://github.com/dergigi/boris/compare/v0.7.2...v0.7.3

View File

@@ -15,7 +15,6 @@ const RELAYS = [
'wss://relay.dergigi.com',
'wss://wot.dergigi.com',
'wss://relay.snort.social',
'wss://relay.current.fyi',
'wss://nostr-pub.wellorder.net',
'wss://purplepag.es',
'wss://relay.primal.net'
@@ -215,12 +214,6 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
const debugEnabled = req.query.debug === '1' || req.headers['x-boris-debug'] === '1'
if (debugEnabled) {
console.log('[article-og] request', JSON.stringify({
naddr,
ua: userAgent || null,
isCrawlerRequest,
path: req.url || null
}))
res.setHeader('X-Boris-Debug', '1')
}
@@ -257,7 +250,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
if (debugEnabled) {
console.log('[article-og] response', JSON.stringify({ mode: 'browser', naddr }))
// Debug mode enabled
}
return res.status(200).send(html)
}
@@ -268,7 +261,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
if (cached && cached.expires > now) {
setCacheHeaders(res)
if (debugEnabled) {
console.log('[article-og] response', JSON.stringify({ mode: 'bot', naddr, cache: true }))
// Debug mode enabled
}
return res.status(200).send(cached.html)
}
@@ -286,7 +279,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
// Send response
setCacheHeaders(res)
if (debugEnabled) {
console.log('[article-og] response', JSON.stringify({ mode: 'bot', naddr, cache: false }))
// Debug mode enabled
}
return res.status(200).send(html)
} catch (err) {
@@ -296,7 +289,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
const html = generateHtml(naddr, null)
setCacheHeaders(res, 3600)
if (debugEnabled) {
console.log('[article-og] response', JSON.stringify({ mode: 'bot-fallback', naddr }))
// Debug mode enabled
}
return res.status(200).send(html)
}

View File

@@ -1,6 +1,6 @@
{
"name": "boris",
"version": "0.8.1",
"version": "0.8.3",
"description": "A minimal nostr client for bookmark management",
"homepage": "https://read.withboris.com/",
"type": "module",

View File

@@ -70,18 +70,14 @@ function AppRoutes({
// Subscribe to contacts controller
useEffect(() => {
console.log('[contacts] 🎧 Subscribing to contacts controller')
const unsubContacts = contactsController.onContacts((contacts) => {
console.log('[contacts] 📥 Received contacts:', contacts.size)
setContacts(contacts)
})
const unsubLoading = contactsController.onLoading((loading) => {
console.log('[contacts] 📥 Loading state:', loading)
setContactsLoading(loading)
})
return () => {
console.log('[contacts] 🔇 Unsubscribing from contacts controller')
unsubContacts()
unsubLoading()
}
@@ -100,25 +96,21 @@ function AppRoutes({
// Load contacts
if (pubkey && contacts.size === 0 && !contactsLoading) {
console.log('[contacts] 🚀 Auto-loading contacts on mount/login')
contactsController.start({ relayPool, pubkey })
}
// Load highlights (controller manages its own state)
if (pubkey && eventStore && !highlightsController.isLoadedFor(pubkey)) {
console.log('[highlights] 🚀 Auto-loading highlights on mount/login')
highlightsController.start({ relayPool, eventStore, pubkey })
}
// Load writings (controller manages its own state)
if (pubkey && eventStore && !writingsController.isLoadedFor(pubkey)) {
console.log('[writings] 🚀 Auto-loading writings on mount/login')
writingsController.start({ relayPool, eventStore, pubkey })
}
// Load reading progress (controller manages its own state)
if (pubkey && eventStore && !readingProgressController.isLoadedFor(pubkey)) {
console.log('[progress] 🚀 Auto-loading reading progress on mount/login')
readingProgressController.start({ relayPool, eventStore, pubkey })
}
@@ -386,40 +378,31 @@ function App() {
// Return an already-resolved promise so upstream await finishes immediately
return Promise.resolve()
}
console.log('[bunker] ✅ Wired NostrConnectSigner to RelayPool publish/subscription (before account load)')
// Create a relay group for better event deduplication and management
pool.group(RELAYS)
console.log('[bunker] Created relay group with', RELAYS.length, 'relays (including local)')
// Load persisted accounts from localStorage
try {
const accountsJson = localStorage.getItem('accounts')
console.log('[bunker] Raw accounts from localStorage:', accountsJson)
const json = JSON.parse(accountsJson || '[]')
console.log('[bunker] Parsed accounts:', json.length, 'accounts')
await accounts.fromJSON(json)
console.log('[bunker] Loaded', accounts.accounts.length, 'accounts from storage')
console.log('[bunker] Account types:', accounts.accounts.map(a => ({ id: a.id, type: a.type })))
// Load active account from storage
const activeId = localStorage.getItem('active')
console.log('[bunker] Active ID from localStorage:', activeId)
if (activeId) {
const account = accounts.getAccount(activeId)
console.log('[bunker] Found account for ID?', !!account, account?.type)
if (account) {
accounts.setActive(activeId)
console.log('[bunker] ✅ Restored active account:', activeId, 'type:', account.type)
} else {
console.warn('[bunker] ⚠️ Active ID found but account not in list')
}
} else {
console.log('[bunker] No active account ID in localStorage')
// No active account ID in localStorage
}
} catch (err) {
console.error('[bunker] ❌ Failed to load accounts from storage:', err)
@@ -444,11 +427,6 @@ function App() {
const reconnectedAccounts = new Set<string>()
const bunkerReconnectSub = accounts.active$.subscribe(async (account) => {
console.log('[bunker] Active account changed:', {
hasAccount: !!account,
type: account?.type,
id: account?.id
})
if (account && account.type === 'nostr-connect') {
const nostrConnectAccount = account as Accounts.NostrConnectAccount<unknown>
@@ -456,23 +434,17 @@ function App() {
try {
if (!(nostrConnectAccount as unknown as { disableQueue?: boolean }).disableQueue) {
(nostrConnectAccount as unknown as { disableQueue?: boolean }).disableQueue = true
console.log('[bunker] ⚙️ Disabled account request queueing for nostr-connect')
}
} catch (err) { console.warn('[bunker] failed to disable queue', err) }
} catch (err) {
// Ignore queue disable errors
}
// Note: for Amber bunker, the remote signer pubkey is the user's pubkey. This is expected.
// Skip if we've already reconnected this account
if (reconnectedAccounts.has(account.id)) {
console.log('[bunker] ⏭️ Already reconnected this account, skipping')
return
}
console.log('[bunker] Account detected. Status:', {
listening: nostrConnectAccount.signer.listening,
isConnected: nostrConnectAccount.signer.isConnected,
hasRemote: !!nostrConnectAccount.signer.remote,
bunkerRelays: nostrConnectAccount.signer.relays
})
try {
// For restored signers, ensure they have the pool's subscription methods
@@ -486,10 +458,9 @@ function App() {
const newBunkerRelays = bunkerRelays.filter(url => !existingRelayUrls.has(url))
if (newBunkerRelays.length > 0) {
console.log('[bunker] Adding bunker relays to pool BEFORE signer recreation:', newBunkerRelays)
pool.group(newBunkerRelays)
} else {
console.log('[bunker] Bunker relays already in pool')
// Bunker relays already in pool
}
const recreatedSigner = new NostrConnectSigner({
@@ -503,13 +474,11 @@ function App() {
try {
const mergedRelays = Array.from(new Set([...(signerData.relays || []), ...RELAYS]))
recreatedSigner.relays = mergedRelays
console.log('[bunker] 🔗 Signer relays merged with app RELAYS:', mergedRelays)
} catch (err) { console.warn('[bunker] failed to merge signer relays', err) }
// Replace the signer on the account
nostrConnectAccount.signer = recreatedSigner
console.log('[bunker] ✅ Signer recreated with pool context')
// Debug: log publish/subscription calls made by signer (decrypt/sign requests)
// IMPORTANT: bind originals to preserve `this` context used internally by the signer
const originalPublish = (recreatedSigner as unknown as { publishMethod: (relays: string[], event: unknown) => unknown }).publishMethod.bind(recreatedSigner)
@@ -531,7 +500,6 @@ function App() {
tags: (event as { tags?: unknown })?.tags,
contentLength: typeof content === 'string' ? content.length : undefined
}
console.log('[bunker] publish via signer:', summary)
try { DebugBus.info('bunker', 'publish', summary) } catch (err) { console.warn('[bunker] failed to log to DebugBus', err) }
} catch (err) { console.warn('[bunker] failed to log publish summary', err) }
// Fire-and-forget publish: trigger the publish but do not return the
@@ -549,7 +517,6 @@ function App() {
const originalSubscribe = (recreatedSigner as unknown as { subscriptionMethod: (relays: string[], filters: unknown[]) => unknown }).subscriptionMethod.bind(recreatedSigner)
;(recreatedSigner as unknown as { subscriptionMethod: (relays: string[], filters: unknown[]) => unknown }).subscriptionMethod = (relays: string[], filters: unknown[]) => {
try {
console.log('[bunker] subscribe via signer:', { relays, filters })
try { DebugBus.info('bunker', 'subscribe', { relays, filters }) } catch (err) { console.warn('[bunker] failed to log subscribe to DebugBus', err) }
} catch (err) { console.warn('[bunker] failed to log subscribe summary', err) }
return originalSubscribe(relays, filters)
@@ -559,20 +526,16 @@ function App() {
// Just ensure the signer is listening for responses - don't call connect() again
// The fromBunkerURI already connected with permissions during login
if (!nostrConnectAccount.signer.listening) {
console.log('[bunker] Opening signer subscription...')
await nostrConnectAccount.signer.open()
console.log('[bunker] ✅ Signer subscription opened')
} else {
console.log('[bunker] ✅ Signer already listening')
// Signer already listening
}
// Attempt a guarded reconnect to ensure Amber authorizes decrypt operations
try {
if (nostrConnectAccount.signer.remote && !reconnectedAccounts.has(account.id)) {
const permissions = getDefaultBunkerPermissions()
console.log('[bunker] Attempting guarded connect() with permissions to ensure decrypt perms', { count: permissions.length })
await nostrConnectAccount.signer.connect(undefined, permissions)
console.log('[bunker] ✅ Guarded connect() succeeded with permissions')
}
} catch (e) {
console.warn('[bunker] ⚠️ Guarded connect() failed:', e)
@@ -581,7 +544,6 @@ function App() {
// Give the subscription a moment to fully establish before allowing decrypt operations
// This ensures the signer is ready to handle and receive responses
await new Promise(resolve => setTimeout(resolve, 100))
console.log("[bunker] Subscription ready after startup delay")
// Fire-and-forget: probe decrypt path to verify Amber responds to NIP-46 decrypt
try {
const withTimeout = async <T,>(p: Promise<T>, ms = 10000): Promise<T> => {
@@ -594,38 +556,27 @@ function App() {
const self = nostrConnectAccount.pubkey
// Try a roundtrip so the bunker can respond successfully
try {
console.log('[bunker] 🔎 Probe nip44 roundtrip (encrypt→decrypt)…')
const cipher44 = await withTimeout(nostrConnectAccount.signer.nip44!.encrypt(self, 'probe-nip44'))
const plain44 = await withTimeout(nostrConnectAccount.signer.nip44!.decrypt(self, cipher44))
console.log('[bunker] 🔎 Probe nip44 responded:', typeof plain44 === 'string' ? plain44 : typeof plain44)
} catch (err) {
console.log('[bunker] 🔎 Probe nip44 result:', err instanceof Error ? err.message : err)
await withTimeout(nostrConnectAccount.signer.nip44!.encrypt(self, 'probe-nip44'))
await withTimeout(nostrConnectAccount.signer.nip44!.decrypt(self, ''))
} catch (_err) {
// Ignore probe errors
}
try {
console.log('[bunker] 🔎 Probe nip04 roundtrip (encrypt→decrypt)…')
const cipher04 = await withTimeout(nostrConnectAccount.signer.nip04!.encrypt(self, 'probe-nip04'))
const plain04 = await withTimeout(nostrConnectAccount.signer.nip04!.decrypt(self, cipher04))
console.log('[bunker] 🔎 Probe nip04 responded:', typeof plain04 === 'string' ? plain04 : typeof plain04)
} catch (err) {
console.log('[bunker] 🔎 Probe nip04 result:', err instanceof Error ? err.message : err)
await withTimeout(nostrConnectAccount.signer.nip04!.encrypt(self, 'probe-nip04'))
await withTimeout(nostrConnectAccount.signer.nip04!.decrypt(self, ''))
} catch (_err) {
// Ignore probe errors
}
}, 0)
} catch (err) {
console.log('[bunker] 🔎 Probe setup failed:', err)
} catch (_err) {
// Ignore signer setup errors
}
// The bunker remembers the permissions from the initial connection
nostrConnectAccount.signer.isConnected = true
console.log('[bunker] Final signer status:', {
listening: nostrConnectAccount.signer.listening,
isConnected: nostrConnectAccount.signer.isConnected,
remote: nostrConnectAccount.signer.remote,
relays: nostrConnectAccount.signer.relays
})
// Mark this account as reconnected
reconnectedAccounts.add(account.id)
console.log('[bunker] 🎉 Signer ready for signing')
} catch (error) {
console.error('[bunker] ❌ Failed to open signer:', error)
}
@@ -639,7 +590,6 @@ function App() {
next: () => {}, // No-op, we don't care about events
error: (err) => console.warn('Keep-alive subscription error:', err)
})
console.log('🔗 Created keep-alive subscription for', RELAYS.length, 'relay(s)')
// Store subscription for cleanup
;(pool as unknown as { _keepAliveSubscription: typeof keepAliveSub })._keepAliveSubscription = keepAliveSub

View File

@@ -34,9 +34,9 @@ const BlogPostCard: React.FC<BlogPostCardProps> = ({ post, href, level, readingP
progressColor = 'var(--color-text)' // Neutral text color (started)
}
// Debug log
// Debug log - reading progress shown as visual indicator
if (readingProgress !== undefined) {
console.log('[progress] 🎴 Card render:', post.title.slice(0, 30), '=> progress:', progressPercent + '%', 'color:', progressColor)
// Reading progress display
}
return (

View File

@@ -22,6 +22,10 @@ import { Hooks } from 'applesauce-react'
import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters'
import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier'
import LoginOptions from './LoginOptions'
import { useEffect } from 'react'
import { readingProgressController } from '../services/readingProgressController'
import { nip19 } from 'nostr-tools'
import { extractUrlsFromContent } from '../services/bookmarkHelpers'
interface BookmarkListProps {
bookmarks: Bookmark[]
@@ -70,6 +74,45 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
return saved === 'flat' ? 'flat' : 'grouped'
})
const activeAccount = Hooks.useActiveAccount()
const [readingProgressMap, setReadingProgressMap] = useState<Map<string, number>>(new Map())
// Subscribe to reading progress updates
useEffect(() => {
// Get initial progress map
setReadingProgressMap(readingProgressController.getProgressMap())
// Subscribe to updates
const unsubProgress = readingProgressController.onProgress(setReadingProgressMap)
return () => {
unsubProgress()
}
}, [])
// Helper to get reading progress for a bookmark
const getBookmarkReadingProgress = (bookmark: IndividualBookmark): number | undefined => {
if (bookmark.kind === 30023) {
// For articles, use naddr as key
const dTag = bookmark.tags.find(t => t[0] === 'd')?.[1]
if (!dTag) return undefined
try {
const naddr = nip19.naddrEncode({
kind: 30023,
pubkey: bookmark.pubkey,
identifier: dTag
})
return readingProgressMap.get(naddr)
} catch (err) {
return undefined
}
}
// For web bookmarks and other types, try to use URL if available
const urls = extractUrlsFromContent(bookmark.content)
if (urls.length > 0) {
return readingProgressMap.get(urls[0])
}
return undefined
}
const toggleGroupingMode = () => {
const newMode = groupingMode === 'grouped' ? 'flat' : 'grouped'
@@ -221,6 +264,7 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
index={index}
onSelectUrl={onSelectUrl}
viewMode={viewMode}
readingProgress={getBookmarkReadingProgress(individualBookmark)}
/>
))}
</div>

View File

@@ -73,15 +73,16 @@ export const CompactView: React.FC<CompactViewProps> = ({
{/* CTA removed */}
</div>
{/* Reading progress indicator for articles */}
{isArticle && readingProgress !== undefined && readingProgress > 0 && (
{/* Reading progress indicator for all bookmark types with reading data */}
{readingProgress !== undefined && readingProgress > 0 && (
<div
style={{
height: '2px',
height: '1px',
width: '100%',
background: 'var(--color-border)',
overflow: 'hidden',
margin: '0'
margin: '0',
marginLeft: '1.5rem'
}}
>
<div

View File

@@ -151,33 +151,18 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
// Callback to save reading position
const handleSavePosition = useCallback(async (position: number) => {
if (!activeAccount || !relayPool || !eventStore || !articleIdentifier) {
console.log('[progress] ⏭️ ContentPanel: Skipping save - missing requirements:', {
hasAccount: !!activeAccount,
hasRelayPool: !!relayPool,
hasEventStore: !!eventStore,
hasIdentifier: !!articleIdentifier
})
return
}
if (!settings?.syncReadingPosition) {
console.log('[progress] ⏭️ ContentPanel: Sync disabled in settings')
return
}
// Check if content is long enough to track reading progress
if (!shouldTrackReadingProgress(html, markdown)) {
console.log('[progress] ⏭️ ContentPanel: Content too short to track reading progress')
return
}
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
console.log('[progress] 💾 ContentPanel: Saving position:', {
position,
percentage: Math.round(position * 100) + '%',
scrollTop,
articleIdentifier: articleIdentifier.slice(0, 50) + '...',
url: selectedUrl?.slice(0, 50)
})
try {
const factory = new EventFactory({ signer: activeAccount })
@@ -192,20 +177,18 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
scrollTop
}
)
console.log('[progress] ✅ ContentPanel: Save completed successfully')
} catch (error) {
console.error('[progress] ❌ ContentPanel: Failed to save reading position:', error)
}
}, [activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition, selectedUrl, html, markdown])
}, [activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition, html, markdown])
const { isReadingComplete, progressPercentage, saveNow } = useReadingPosition({
const { progressPercentage, saveNow } = useReadingPosition({
enabled: isTextContent,
syncEnabled: settings?.syncReadingPosition !== false,
onSave: handleSavePosition,
onReadingComplete: () => {
// Auto-mark as read when reading is complete (if enabled in settings)
if (activeAccount && !isMarkedAsRead && settings?.autoMarkAsReadOnCompletion) {
console.log('[progress] 📖 Auto-marking as read on completion')
handleMarkAsRead()
}
}
@@ -213,36 +196,17 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
// Log sync status when it changes
useEffect(() => {
console.log('[progress] 📊 ContentPanel reading position sync status:', {
enabled: isTextContent,
syncEnabled: settings?.syncReadingPosition !== false,
hasAccount: !!activeAccount,
hasRelayPool: !!relayPool,
hasEventStore: !!eventStore,
hasArticleIdentifier: !!articleIdentifier,
currentProgress: progressPercentage + '%'
})
}, [isTextContent, settings?.syncReadingPosition, activeAccount, relayPool, eventStore, articleIdentifier, progressPercentage])
// Load saved reading position when article loads
useEffect(() => {
if (!isTextContent || !activeAccount || !relayPool || !eventStore || !articleIdentifier) {
console.log('⏭️ [ContentPanel] Skipping position restore - missing requirements:', {
isTextContent,
hasAccount: !!activeAccount,
hasRelayPool: !!relayPool,
hasEventStore: !!eventStore,
hasIdentifier: !!articleIdentifier
})
return
}
if (settings?.syncReadingPosition === false) {
console.log('⏭️ [ContentPanel] Sync disabled in settings - not restoring position')
return
}
console.log('📖 [ContentPanel] Loading position for article:', selectedUrl?.slice(0, 50))
const loadPosition = async () => {
try {
const savedPosition = await loadReadingPosition(
@@ -253,7 +217,6 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
)
if (savedPosition && savedPosition.position > 0.05 && savedPosition.position < 1) {
console.log('🎯 [ContentPanel] Restoring position:', Math.round(savedPosition.position * 100) + '%')
// Wait for content to be fully rendered before scrolling
setTimeout(() => {
const documentHeight = document.documentElement.scrollHeight
@@ -264,14 +227,12 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
top: scrollTop,
behavior: 'smooth'
})
console.log('✅ [ContentPanel] Restored to position:', Math.round(savedPosition.position * 100) + '%', 'scrollTop:', scrollTop)
}, 500) // Give content time to render
} else if (savedPosition) {
if (savedPosition.position === 1) {
console.log('✅ [ContentPanel] Article completed (100%), starting from top')
// Article was completed, start from top
} else {
console.log('⏭️ [ContentPanel] Position too early (<5%):', Math.round(savedPosition.position * 100) + '%')
// Position was too early, skip restore
}
}
} catch (error) {
@@ -648,14 +609,12 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
activeAccount,
relayPool
)
console.log('✅ Marked nostr article as read')
} else if (selectedUrl) {
await createWebsiteReaction(
selectedUrl,
activeAccount,
relayPool
)
console.log('✅ Marked website as read')
}
} catch (error) {
console.error('Failed to mark as read:', error)
@@ -689,7 +648,8 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
{isTextContent && (
<ReadingProgressIndicator
progress={progressPercentage}
isComplete={isReadingComplete}
// Consider complete only at 95%+
isComplete={progressPercentage >= 95}
showPercentage={true}
isSidebarCollapsed={isSidebarCollapsed}
isHighlightsCollapsed={isHighlightsCollapsed}

View File

@@ -19,6 +19,7 @@ import { useSettings } from '../hooks/useSettings'
import { fetchHighlights, fetchHighlightsFromAuthors } from '../services/highlightService'
import { contactsController } from '../services/contactsController'
import { writingsController } from '../services/writingsController'
import { readingProgressController } from '../services/readingProgressController'
import { fetchBlogPostsFromAuthors, BlogPostPreview } from '../services/exploreService'
const defaultPayload = 'The quick brown fox jumps over the lazy dog.'
@@ -102,6 +103,21 @@ const Debug: React.FC<DebugProps> = ({
const [tLoadWritings, setTLoadWritings] = useState<number | null>(null)
const [tFirstWriting, setTFirstWriting] = useState<number | null>(null)
// Reading Progress loading state
const [isLoadingReadingProgress, setIsLoadingReadingProgress] = useState(false)
const [readingProgressEvents, setReadingProgressEvents] = useState<NostrEvent[]>([])
const [tLoadReadingProgress, setTLoadReadingProgress] = useState<number | null>(null)
const [tFirstReadingProgress, setTFirstReadingProgress] = useState<number | null>(null)
// Mark-as-read reactions loading state
const [isLoadingMarkAsRead, setIsLoadingMarkAsRead] = useState(false)
const [markAsReadReactions, setMarkAsReadReactions] = useState<NostrEvent[]>([])
const [tLoadMarkAsRead, setTLoadMarkAsRead] = useState<number | null>(null)
const [tFirstMarkAsRead, setTFirstMarkAsRead] = useState<number | null>(null)
// Deduplicated reading progress from controller
const [deduplicatedProgressMap, setDeduplicatedProgressMap] = useState<Map<string, number>>(new Map())
// Live timing state
const [liveTiming, setLiveTiming] = useState<{
nip44?: { type: 'encrypt' | 'decrypt'; startTime: number }
@@ -109,6 +125,8 @@ const Debug: React.FC<DebugProps> = ({
loadBookmarks?: { startTime: number }
decryptBookmarks?: { startTime: number }
loadHighlights?: { startTime: number }
loadReadingProgress?: { startTime: number }
loadMarkAsRead?: { startTime: number }
}>({})
// Web of Trust state
@@ -310,10 +328,6 @@ const Debug: React.FC<DebugProps> = ({
// Subscribe to decrypt complete events for Debug UI display
const unsubscribeDecrypt = bookmarkController.onDecryptComplete((eventId, publicCount, privateCount) => {
console.log('[bunker] ✅ Auto-decrypted:', eventId.slice(0, 8), {
public: publicCount,
private: privateCount
})
setDecryptedEvents(prev => new Map(prev).set(eventId, {
public: publicCount,
private: privateCount
@@ -728,6 +742,150 @@ const Debug: React.FC<DebugProps> = ({
setTFirstWriting(null)
}
const handleLoadReadingProgress = async () => {
if (!relayPool || !eventStore || !activeAccount?.pubkey) {
DebugBus.warn('debug', 'Please log in to load reading progress')
return
}
try {
setIsLoadingReadingProgress(true)
setReadingProgressEvents([])
setTLoadReadingProgress(null)
setTFirstReadingProgress(null)
setDeduplicatedProgressMap(new Map())
DebugBus.info('debug', 'Loading reading progress events...')
const start = performance.now()
let firstEventTime: number | null = null
setLiveTiming(prev => ({ ...prev, loadReadingProgress: { startTime: start } }))
const { queryEvents } = await import('../services/dataFetch')
const { KINDS } = await import('../config/kinds')
// Load raw events for display
const rawEvents: NostrEvent[] = []
const rawQueryPromise = queryEvents(relayPool, { kinds: [KINDS.ReadingProgress], authors: [activeAccount.pubkey] }, {
onEvent: (evt) => {
if (firstEventTime === null) {
firstEventTime = performance.now() - start
setTFirstReadingProgress(Math.round(firstEventTime))
}
rawEvents.push(evt)
setReadingProgressEvents([...rawEvents])
}
})
// Load deduplicated results via controller
const unsubProgress = readingProgressController.onProgress((progressMap) => {
setDeduplicatedProgressMap(new Map(progressMap))
})
// Run both in parallel
await Promise.all([
rawQueryPromise,
readingProgressController.start({ relayPool, eventStore, pubkey: activeAccount.pubkey, force: true })
])
unsubProgress()
const elapsed = Math.round(performance.now() - start)
setTLoadReadingProgress(elapsed)
setLiveTiming(prev => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
const { loadReadingProgress, ...rest } = prev
return rest
})
const finalMap = readingProgressController.getProgressMap()
DebugBus.info('debug', `Loaded ${rawEvents.length} raw events, deduplicated to ${finalMap.size} articles in ${elapsed}ms`)
} catch (err) {
console.error('Failed to load reading progress:', err)
DebugBus.error('debug', `Failed to load reading progress: ${err instanceof Error ? err.message : String(err)}`)
} finally {
setIsLoadingReadingProgress(false)
}
}
const handleClearReadingProgress = () => {
setReadingProgressEvents([])
setTLoadReadingProgress(null)
setTFirstReadingProgress(null)
setDeduplicatedProgressMap(new Map())
DebugBus.info('debug', 'Cleared reading progress data')
}
const handleLoadMarkAsReadReactions = async () => {
if (!relayPool || !activeAccount?.pubkey) {
DebugBus.warn('debug', 'Please log in to load mark-as-read reactions')
return
}
try {
setIsLoadingMarkAsRead(true)
setMarkAsReadReactions([])
setTLoadMarkAsRead(null)
setTFirstMarkAsRead(null)
DebugBus.info('debug', 'Loading mark-as-read reactions...')
const start = performance.now()
let firstEventTime: number | null = null
setLiveTiming(prev => ({ ...prev, loadMarkAsRead: { startTime: start } }))
const { queryEvents } = await import('../services/dataFetch')
const { MARK_AS_READ_EMOJI } = await import('../services/reactionService')
// Load both kind:7 (reactions to events) and kind:17 (reactions to URLs)
const [kind7Events, kind17Events] = await Promise.all([
queryEvents(relayPool, { kinds: [7], authors: [activeAccount.pubkey] }, {
onEvent: (evt) => {
if (evt.content === MARK_AS_READ_EMOJI) {
if (firstEventTime === null) {
firstEventTime = performance.now() - start
setTFirstMarkAsRead(Math.round(firstEventTime))
}
setMarkAsReadReactions(prev => [...prev, evt])
}
}
}),
queryEvents(relayPool, { kinds: [17], authors: [activeAccount.pubkey] }, {
onEvent: (evt) => {
if (evt.content === MARK_AS_READ_EMOJI) {
if (firstEventTime === null) {
firstEventTime = performance.now() - start
setTFirstMarkAsRead(Math.round(firstEventTime))
}
setMarkAsReadReactions(prev => [...prev, evt])
}
}
})
])
const totalEvents = kind7Events.length + kind17Events.length
const elapsed = Math.round(performance.now() - start)
setTLoadMarkAsRead(elapsed)
setLiveTiming(prev => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
const { loadMarkAsRead, ...rest } = prev
return rest
})
DebugBus.info('debug', `Loaded ${totalEvents} mark-as-read reactions in ${elapsed}ms`)
} catch (err) {
console.error('Failed to load mark-as-read reactions:', err)
DebugBus.error('debug', `Failed to load mark-as-read reactions: ${err instanceof Error ? err.message : String(err)}`)
} finally {
setIsLoadingMarkAsRead(false)
}
}
const handleClearMarkAsRead = () => {
setMarkAsReadReactions([])
setTLoadMarkAsRead(null)
setTFirstMarkAsRead(null)
DebugBus.info('debug', 'Cleared mark-as-read reactions data')
}
const handleLoadFriendsList = async () => {
if (!relayPool || !activeAccount?.pubkey) {
DebugBus.warn('debug', 'Please log in to load friends list')
@@ -742,7 +900,6 @@ const Debug: React.FC<DebugProps> = ({
// Subscribe to controller updates to see streaming
const unsubscribe = contactsController.onContacts((contacts) => {
console.log('[debug] Received contacts update:', contacts.size)
setFriendsPubkeys(new Set(contacts))
})
@@ -1353,6 +1510,194 @@ const Debug: React.FC<DebugProps> = ({
)}
</div>
{/* Reading Progress Loading Section */}
<div className="settings-section">
<h3 className="section-title">Reading Progress Loading</h3>
<div className="text-sm opacity-70 mb-3">Test reading progress loading (kind: 39802) for the logged-in user</div>
<div className="flex gap-2 mb-3 items-center">
<button
className="btn btn-primary"
onClick={handleLoadReadingProgress}
disabled={isLoadingReadingProgress || !relayPool || !activeAccount}
>
{isLoadingReadingProgress ? (
<>
<FontAwesomeIcon icon={faSpinner} className="animate-spin mr-2" />
Loading...
</>
) : (
'Load Reading Progress'
)}
</button>
<button
className="btn btn-secondary ml-auto"
onClick={handleClearReadingProgress}
disabled={readingProgressEvents.length === 0}
>
Clear
</button>
</div>
<div className="mb-3 flex gap-2 flex-wrap">
<Stat label="total" value={tLoadReadingProgress} />
<Stat label="first event" value={tFirstReadingProgress} />
</div>
{readingProgressEvents.length > 0 && (
<div className="mb-3">
<div className="text-sm opacity-70 mb-2">Loaded Reading Progress ({readingProgressEvents.length}):</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{readingProgressEvents.map((evt, idx) => {
const dTag = evt.tags?.find((t: string[]) => t[0] === 'd')?.[1]
const aTag = evt.tags?.find((t: string[]) => t[0] === 'a')?.[1]
const content = evt.content || ''
return (
<div key={idx} className="font-mono text-xs p-2 bg-gray-100 dark:bg-gray-800 rounded">
<div className="font-semibold mb-1">Reading Progress #{idx + 1}</div>
<div className="opacity-70 mb-1">
<div>Author: {evt.pubkey.slice(0, 16)}...</div>
<div>Created: {new Date(evt.created_at * 1000).toLocaleString()}</div>
</div>
<div className="mt-1">
{dTag && <div>d-tag: {dTag}</div>}
{aTag && <div className="text-[11px] opacity-70">#a: {aTag}</div>}
{content && <div>Progress: {content}</div>}
</div>
<div className="opacity-50 mt-1 text-[10px] break-all">ID: {evt.id}</div>
</div>
)
})}
</div>
</div>
)}
{deduplicatedProgressMap.size > 0 && (
<div className="mb-3">
<div className="text-sm opacity-70 mb-2">Deduplicated Reading Progress ({deduplicatedProgressMap.size} articles):</div>
{/* Category breakdown */}
<div className="mb-3 p-2 bg-purple-50 dark:bg-purple-900/20 rounded border border-purple-200 dark:border-purple-700">
<div className="text-sm font-semibold mb-2">Breakdown by Category:</div>
<div className="space-y-1">
{(() => {
let unopened = 0, started = 0, reading = 0, completed = 0
for (const progress of deduplicatedProgressMap.values()) {
if (progress === 0) unopened++
else if (progress > 0 && progress <= 0.10) started++
else if (progress > 0.10 && progress <= 0.94) reading++
else if (progress >= 0.95) completed++
}
return (
<>
<div className="flex justify-between text-xs">
<span>Unopened (0%):</span>
<span className="font-semibold">{unopened}</span>
</div>
<div className="flex justify-between text-xs">
<span>Started (0% &lt; progress 10%):</span>
<span className="font-semibold">{started}</span>
</div>
<div className="flex justify-between text-xs bg-green-100 dark:bg-green-900/30 px-1 py-0.5 rounded">
<span>Reading (10% &lt; progress 94%) :</span>
<span className="font-semibold text-green-700 dark:text-green-400">{reading}</span>
</div>
<div className="flex justify-between text-xs">
<span>Completed ( 95%):</span>
<span className="font-semibold">{completed}</span>
</div>
</>
)
})()}
</div>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{Array.from(deduplicatedProgressMap.entries()).map(([articleId, progress], idx) => {
return (
<div key={idx} className="font-mono text-xs p-2 bg-blue-50 dark:bg-blue-900/20 rounded border border-blue-200 dark:border-blue-700">
<div className="font-semibold mb-1">Article #{idx + 1}</div>
<div className="mt-1">
<div className="break-all">ID: {articleId}</div>
<div className="mt-1">
<div className="text-[11px] opacity-70">Progress: {(progress * 100).toFixed(1)}%</div>
<div className="w-full bg-gray-300 dark:bg-gray-700 rounded-full h-1.5 mt-1 overflow-hidden">
<div
className="bg-blue-600 h-full"
style={{ width: `${progress * 100}%` }}
/>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
)}
</div>
{/* Mark-as-read Reactions Loading Section */}
<div className="settings-section">
<h3 className="section-title">Mark-as-read Reactions Loading</h3>
<div className="text-sm opacity-70 mb-3">Test loading mark-as-read reactions (kind: 7 and 17) with the MARK_AS_READ_EMOJI for the logged-in user</div>
<div className="flex gap-2 mb-3 items-center">
<button
className="btn btn-primary"
onClick={handleLoadMarkAsReadReactions}
disabled={isLoadingMarkAsRead || !relayPool || !activeAccount}
>
{isLoadingMarkAsRead ? (
<>
<FontAwesomeIcon icon={faSpinner} className="animate-spin mr-2" />
Loading...
</>
) : (
'Load Mark-as-read Reactions'
)}
</button>
<button
className="btn btn-secondary ml-auto"
onClick={handleClearMarkAsRead}
disabled={markAsReadReactions.length === 0}
>
Clear
</button>
</div>
<div className="mb-3 flex gap-2 flex-wrap">
<Stat label="total" value={tLoadMarkAsRead} />
<Stat label="first event" value={tFirstMarkAsRead} />
</div>
{markAsReadReactions.length > 0 && (
<div className="mb-3">
<div className="text-sm opacity-70 mb-2">Loaded Mark-as-read Reactions ({markAsReadReactions.length}):</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{markAsReadReactions.map((evt, idx) => {
const eTag = evt.tags?.find((t: string[]) => t[0] === 'e')?.[1]
const rTag = evt.tags?.find((t: string[]) => t[0] === 'r')?.[1]
const pTag = evt.tags?.find((t: string[]) => t[0] === 'p')?.[1]
return (
<div key={idx} className="font-mono text-xs p-2 bg-gray-100 dark:bg-gray-800 rounded">
<div className="font-semibold mb-1">Mark-as-read Reaction #{idx + 1}</div>
<div className="opacity-70 mb-1">
<div>Kind: {evt.kind}</div>
<div>Author: {evt.pubkey.slice(0, 16)}...</div>
<div>Created: {new Date(evt.created_at * 1000).toLocaleString()}</div>
</div>
<div className="mt-1">
<div>Emoji: {evt.content}</div>
{eTag && <div className="text-[11px] opacity-70">#e: {eTag.slice(0, 16)}...</div>}
{rTag && <div className="text-[11px] opacity-70">#r: {rTag.length > 60 ? rTag.substring(0, 60) + '...' : rTag}</div>}
{pTag && <div className="text-[11px] opacity-70">#p: {pTag.slice(0, 16)}...</div>}
</div>
<div className="opacity-50 mt-1 text-[10px] break-all">ID: {evt.id}</div>
</div>
)
})}
</div>
</div>
)}
</div>
{/* Web of Trust Section */}
<div className="settings-section">
<h3 className="section-title">Web of Trust</h3>

View File

@@ -178,12 +178,10 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
useEffect(() => {
// Get initial state immediately
const initialMap = readingProgressController.getProgressMap()
console.log('[progress] 🎯 Explore: Initial progress map size:', initialMap.size)
setReadingProgressMap(initialMap)
// Subscribe to updates
const unsubProgress = readingProgressController.onProgress((newMap) => {
console.log('[progress] 🎯 Explore: Received progress update, size:', newMap.size)
setReadingProgressMap(newMap)
})
@@ -612,7 +610,6 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
const getReadingProgress = useCallback((post: BlogPostPreview): number | undefined => {
const dTag = post.event.tags.find(t => t[0] === 'd')?.[1]
if (!dTag) {
console.log('[progress] ⚠️ No d-tag for post:', post.title)
return undefined
}
@@ -624,16 +621,6 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
})
const progress = readingProgressMap.get(naddr)
// Only log first lookup to avoid spam, or when found
if (progress || readingProgressMap.size === 0) {
console.log('[progress] 🔍 Looking up:', {
title: post.title.slice(0, 30),
naddr: naddr.slice(0, 80),
mapSize: readingProgressMap.size,
mapKeys: readingProgressMap.size > 0 ? Array.from(readingProgressMap.keys()).slice(0, 3).map(k => k.slice(0, 80)) : [],
progress: progress ? Math.round(progress * 100) + '%' : 'not found'
})
}
return progress
} catch (err) {
console.error('[progress] ❌ Error encoding naddr:', err)

View File

@@ -27,7 +27,6 @@ export const HighlightCitation: React.FC<HighlightCitationProps> = ({
// Fallback: extract directly from p tag
const pTag = highlight.tags.find(t => t[0] === 'p')
if (pTag && pTag[1]) {
console.log('📝 Found author from p tag:', pTag[1])
return pTag[1]
}
@@ -45,6 +44,12 @@ export const HighlightCitation: React.FC<HighlightCitationProps> = ({
try {
if (!highlight.eventReference) return
// Skip if it's a raw event ID (hex string without colons)
// Raw event IDs cannot be decoded to nadrs without additional context
if (!highlight.eventReference.includes(':') && !highlight.eventReference.startsWith('naddr')) {
return
}
// Convert eventReference to naddr if needed
let naddr: string
if (highlight.eventReference.includes(':')) {

View File

@@ -348,11 +348,9 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
// Publish to all configured relays - let the relay pool handle connection state
const targetRelays = RELAYS
console.log('📡 Rebroadcasting highlight to', targetRelays.length, 'relay(s):', targetRelays)
await relayPool.publish(targetRelays, event)
console.log('✅ Rebroadcast successful!')
// Update the highlight with new relay info
const isLocalOnly = areAllRelaysLocal(targetRelays)
@@ -449,7 +447,6 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
relayPool
)
console.log('✅ Highlight deletion request published')
// Notify parent to remove this highlight from the list
if (onHighlightDelete) {

View File

@@ -11,8 +11,8 @@ import { Highlight } from '../types/highlights'
import { HighlightItem } from './HighlightItem'
import { highlightsController } from '../services/highlightsController'
import { writingsController } from '../services/writingsController'
import { fetchAllReads, ReadItem } from '../services/readsService'
import { fetchLinks } from '../services/linksService'
import { ReadItem } from '../services/readsService'
import { BlogPostPreview } from '../services/exploreService'
import { Bookmark, IndividualBookmark } from '../types/bookmarks'
import AuthorCard from './AuthorCard'
@@ -28,9 +28,7 @@ import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters'
import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier'
import ReadingProgressFilters, { ReadingProgressFilterType } from './ReadingProgressFilters'
import { filterByReadingProgress } from '../utils/readingProgressUtils'
import { deriveReadsFromBookmarks } from '../utils/readsFromBookmarks'
import { deriveLinksFromBookmarks } from '../utils/linksFromBookmarks'
import { mergeReadItem } from '../utils/readItemMerge'
import { readingProgressController } from '../services/readingProgressController'
interface MeProps {
@@ -44,7 +42,7 @@ interface MeProps {
type TabType = 'highlights' | 'reading-list' | 'reads' | 'links' | 'writings'
// Valid reading progress filters
const VALID_FILTERS: ReadingProgressFilterType[] = ['all', 'unopened', 'started', 'reading', 'completed', 'highlighted']
const VALID_FILTERS: ReadingProgressFilterType[] = ['all', 'unopened', 'started', 'reading', 'completed', 'highlighted', 'emoji']
const Me: React.FC<MeProps> = ({
relayPool,
@@ -159,12 +157,81 @@ const Me: React.FC<MeProps> = ({
setReadingProgressMap(readingProgressController.getProgressMap())
// Subscribe to updates
const unsubProgress = readingProgressController.onProgress(setReadingProgressMap)
const unsubProgress = readingProgressController.onProgress((progressMap) => {
const readItems: ReadItem[] = Array.from(progressMap.entries()).map(([id, progress]) => ({
id,
source: 'reading-progress',
type: 'article',
readingProgress: progress,
markedAsRead: readingProgressController.isMarkedAsRead(id),
readingTimestamp: Math.floor(Date.now() / 1000)
}))
// Include items that are only marked-as-read (no progress event yet)
const markedIds = readingProgressController.getMarkedAsReadIds()
for (const id of markedIds) {
if (!readItems.find(i => i.id === id)) {
const isArticle = id.startsWith('naddr1')
readItems.push({
id,
source: 'marked-as-read',
type: isArticle ? 'article' : 'external',
url: isArticle ? undefined : id,
markedAsRead: true,
readingTimestamp: Math.floor(Date.now() / 1000)
})
}
}
const readsMap = new Map(readItems.map(item => [item.id, item]))
setReadsMap(readsMap)
setReads(readItems)
})
return () => {
unsubProgress()
}
}, [])
// Subscribe to marked-as-read changes and rebuild reads list
useEffect(() => {
const unsubMarkedAsRead = readingProgressController.onMarkedAsReadChanged(() => {
// Rebuild reads list including marked-as-read-only items
const progressMap = readingProgressController.getProgressMap()
const readItems: ReadItem[] = Array.from(progressMap.entries()).map(([id, progress]) => ({
id,
source: 'reading-progress',
type: 'article',
readingProgress: progress,
markedAsRead: readingProgressController.isMarkedAsRead(id),
readingTimestamp: Math.floor(Date.now() / 1000)
}))
// Include items that are only marked-as-read (no progress event yet)
const markedIds = readingProgressController.getMarkedAsReadIds()
for (const id of markedIds) {
if (!readItems.find(i => i.id === id)) {
const isArticle = id.startsWith('naddr1')
readItems.push({
id,
source: 'marked-as-read',
type: isArticle ? 'article' : 'external',
url: isArticle ? undefined : id,
markedAsRead: true,
readingTimestamp: Math.floor(Date.now() / 1000)
})
}
}
const readsMap = new Map(readItems.map(item => [item.id, item]))
setReadsMap(readsMap)
setReads(readItems)
})
return () => {
unsubMarkedAsRead()
}
}, [])
// Load reading progress data for writings tab
useEffect(() => {
@@ -232,47 +299,70 @@ const Me: React.FC<MeProps> = ({
try {
if (!hasBeenLoaded) setLoading(true)
// Derive reads from bookmarks immediately (bookmarks come from centralized loading in App.tsx)
const initialReads = deriveReadsFromBookmarks(bookmarks)
const initialMap = new Map(initialReads.map(item => [item.id, item]))
setReadsMap(initialMap)
setReads(initialReads)
// Reads come from centralized readingProgressController (already loaded in App.tsx)
// It provides deduped reading progress per article
const progressMap = readingProgressController.getProgressMap()
// Convert progress map to ReadItems
const readItems: ReadItem[] = Array.from(progressMap.entries()).map(([id, progress]) => ({
id,
source: 'reading-progress',
type: 'article',
readingProgress: progress,
markedAsRead: readingProgressController.isMarkedAsRead(id),
readingTimestamp: Math.floor(Date.now() / 1000)
}))
// Include items that are only marked-as-read (no progress event yet)
const markedIds = readingProgressController.getMarkedAsReadIds()
for (const id of markedIds) {
if (!readItems.find(i => i.id === id)) {
const isArticle = id.startsWith('naddr1')
readItems.push({
id,
source: 'marked-as-read',
type: isArticle ? 'article' : 'external',
url: isArticle ? undefined : id,
markedAsRead: true,
readingTimestamp: Math.floor(Date.now() / 1000)
})
}
}
const readsMap = new Map(readItems.map(item => [item.id, item]))
setReadsMap(readsMap)
setReads(readItems)
setLoadedTabs(prev => new Set(prev).add('reads'))
if (!hasBeenLoaded) setLoading(false)
// Background enrichment: merge reading progress and mark-as-read
// Only update items that are already in our map
fetchAllReads(relayPool, viewingPubkey, bookmarks, (item) => {
console.log('📈 [Reads] Enrichment item received:', {
id: item.id.slice(0, 20) + '...',
progress: item.readingProgress,
hasProgress: item.readingProgress !== undefined && item.readingProgress > 0
})
setReadsMap(prevMap => {
// Only update if item exists in our current map
if (!prevMap.has(item.id)) {
console.log('⚠️ [Reads] Item not in map, skipping:', item.id.slice(0, 20) + '...')
return prevMap
}
const newMap = new Map(prevMap)
const merged = mergeReadItem(newMap, item)
if (merged) {
console.log('✅ [Reads] Merged progress:', item.id.slice(0, 20) + '...', item.readingProgress)
// Update reads array after map is updated
setReads(Array.from(newMap.values()))
return newMap
}
return prevMap
})
}).catch(err => console.warn('Failed to enrich reads:', err))
} catch (err) {
console.error('Failed to load reads:', err)
if (!hasBeenLoaded) setLoading(false)
}
}
// Subscribe to reading progress updates
useEffect(() => {
const unsubProgress = readingProgressController.onProgress((progressMap) => {
const readItems: ReadItem[] = Array.from(progressMap.entries()).map(([id, progress]) => ({
id,
source: 'reading-progress',
type: 'article',
readingProgress: progress,
markedAsRead: readingProgressController.isMarkedAsRead(id),
readingTimestamp: Math.floor(Date.now() / 1000)
}))
const readsMap = new Map(readItems.map(item => [item.id, item]))
setReadsMap(readsMap)
setReads(readItems)
})
return () => {
unsubProgress()
}
}, [])
const loadLinksTab = async () => {
if (!viewingPubkey || !activeAccount) return
@@ -298,12 +388,13 @@ const Me: React.FC<MeProps> = ({
if (!prevMap.has(item.id)) return prevMap
const newMap = new Map(prevMap)
if (mergeReadItem(newMap, item)) {
// Update links array after map is updated
setLinks(Array.from(newMap.values()))
return newMap
if (item.type === 'article' && item.author) {
const progress = readingProgressMap.get(item.id)
if (progress !== undefined) {
newMap.set(item.id, { ...item, readingProgress: progress })
}
}
return prevMap
return newMap
})
}).catch(err => console.warn('Failed to enrich links:', err))

View File

@@ -68,12 +68,10 @@ const Profile: React.FC<ProfileProps> = ({
useEffect(() => {
// Get initial state immediately
const initialMap = readingProgressController.getProgressMap()
console.log('[progress] 🎯 Profile: Initial progress map size:', initialMap.size)
setReadingProgressMap(initialMap)
// Subscribe to updates
const unsubProgress = readingProgressController.onProgress((newMap) => {
console.log('[progress] 🎯 Profile: Received progress update, size:', newMap.size)
setReadingProgressMap(newMap)
})
@@ -100,12 +98,11 @@ const Profile: React.FC<ProfileProps> = ({
useEffect(() => {
if (!pubkey || !relayPool || !eventStore) return
console.log('🔄 [Profile] Background fetching highlights and writings for', pubkey.slice(0, 8))
// Fetch highlights in background
fetchHighlights(relayPool, pubkey, undefined, undefined, false, eventStore)
.then(highlights => {
console.log('✅ [Profile] Fetched', highlights.length, 'highlights')
.then(() => {
// Highlights fetched
})
.catch(err => {
console.warn('⚠️ [Profile] Failed to fetch highlights:', err)
@@ -115,7 +112,6 @@ const Profile: React.FC<ProfileProps> = ({
fetchBlogPostsFromAuthors(relayPool, [pubkey], RELAYS, undefined, null)
.then(writings => {
writings.forEach(w => eventStore.add(w.event))
console.log('✅ [Profile] Fetched', writings.length, 'writings')
})
.catch(err => {
console.warn('⚠️ [Profile] Failed to fetch writings:', err)
@@ -157,14 +153,9 @@ const Profile: React.FC<ProfileProps> = ({
// Only log when found or map is empty
if (progress || readingProgressMap.size === 0) {
console.log('[progress] 🔍 Profile lookup:', {
title: post.title?.slice(0, 30),
naddr: naddr.slice(0, 80),
mapSize: readingProgressMap.size,
mapKeys: readingProgressMap.size > 0 ? Array.from(readingProgressMap.keys()).slice(0, 3).map(k => k.slice(0, 80)) : [],
progress: progress ? Math.round(progress * 100) + '%' : 'not found'
})
// Progress found or map is empty
}
return progress
} catch (err) {
return undefined

View File

@@ -1,9 +1,10 @@
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBookOpen, faCheckCircle, faAsterisk, faHighlighter } from '@fortawesome/free-solid-svg-icons'
import { faBook } from '@fortawesome/free-solid-svg-icons'
import { faEnvelope, faEnvelopeOpen } from '@fortawesome/free-regular-svg-icons'
export type ReadingProgressFilterType = 'all' | 'unopened' | 'started' | 'reading' | 'completed' | 'highlighted'
export type ReadingProgressFilterType = 'all' | 'unopened' | 'started' | 'reading' | 'completed' | 'highlighted' | 'emoji'
interface ReadingProgressFiltersProps {
selectedFilter: ReadingProgressFilterType
@@ -13,11 +14,13 @@ interface ReadingProgressFiltersProps {
const ReadingProgressFilters: React.FC<ReadingProgressFiltersProps> = ({ selectedFilter, onFilterChange }) => {
const filters = [
{ type: 'all' as const, icon: faAsterisk, label: 'All' },
{ type: 'highlighted' as const, icon: faHighlighter, label: 'Highlighted' },
{ type: 'unopened' as const, icon: faEnvelope, label: 'Unopened' },
{ type: 'started' as const, icon: faEnvelopeOpen, label: 'Started' },
{ type: 'reading' as const, icon: faBookOpen, label: 'Reading' },
{ type: 'highlighted' as const, icon: faHighlighter, label: 'Highlighted' },
{ type: 'completed' as const, icon: faCheckCircle, label: 'Completed' }
{ type: 'completed' as const, icon: faCheckCircle, label: 'Completed' },
// Emoji-marked items (marked via reaction emoji)
{ type: 'emoji' as const, icon: faBook, label: 'Emoji' }
]
return (
@@ -31,6 +34,8 @@ const ReadingProgressFilters: React.FC<ReadingProgressFiltersProps> = ({ selecte
activeStyle = { color: '#10b981' } // green
} else if (filter.type === 'highlighted') {
activeStyle = { color: '#fde047' } // yellow
} else if (filter.type === 'emoji') {
activeStyle = { color: '#60a5fa' } // blue accent
}
}

View File

@@ -50,16 +50,8 @@ export const RelayStatusIndicator: React.FC<RelayStatusIndicatorProps> = ({
// Debug logging
useEffect(() => {
console.log('🔌 Relay Status Indicator:', {
mode: isConnecting ? 'CONNECTING' : offlineMode ? 'OFFLINE' : localOnlyMode ? 'LOCAL_ONLY' : 'ONLINE',
totalStatuses: relayStatuses.length,
connectedCount: connectedUrls.length,
connectedUrls: connectedUrls.map(u => u.replace(/^wss?:\/\//, '')),
hasLocalRelay,
hasRemoteRelay,
isConnecting
})
}, [offlineMode, localOnlyMode, connectedUrls, relayStatuses.length, hasLocalRelay, hasRemoteRelay, isConnecting])
// Mode and relay status determined
}, [isConnecting, offlineMode, localOnlyMode, relayStatuses, hasLocalRelay, hasRemoteRelay])
// Don't show indicator when fully connected (but show when connecting)
if (!localOnlyMode && !offlineMode && !isConnecting) return null

View File

@@ -27,7 +27,7 @@ const PWASettings: React.FC<PWASettingsProps> = ({ settings, onUpdate, onClose }
if (isInstalled) return
const success = await installApp()
if (success) {
console.log('App installed successfully')
// Installation successful
}
}

View File

@@ -14,7 +14,6 @@ export const RELAYS = [
'wss://relay.dergigi.com',
'wss://wot.dergigi.com',
'wss://relay.snort.social',
'wss://relay.current.fyi',
'wss://nostr-pub.wellorder.net',
'wss://purplepag.es',
'wss://relay.primal.net',

View File

@@ -43,21 +43,14 @@ export function useAdaptiveTextColor(imageUrl: string | undefined): AdaptiveText
height: Math.floor(height * 0.25)
})
console.log('Adaptive color detected:', {
hex: color.hex,
rgb: color.rgb,
isLight: color.isLight,
isDark: color.isDark
})
// Color analysis complete
// Use library's built-in isLight check for optimal contrast
if (color.isLight) {
console.log('Light background detected, using black text')
setColors({
textColor: '#000000'
})
} else {
console.log('Dark background detected, using white text')
setColors({
textColor: '#ffffff'
})

View File

@@ -64,8 +64,6 @@ export function useArticleLoader({
setCurrentArticleEventId(article.event.id)
setCurrentArticle?.(article.event)
console.log('📰 Article loaded:', article.title)
console.log('📍 Coordinate:', articleCoordinate)
// Set reader loading to false immediately after article content is ready
// Don't wait for highlights to finish loading
@@ -76,23 +74,21 @@ export function useArticleLoader({
try {
setHighlightsLoading(true)
setHighlights([]) // Clear old highlights
const highlightsMap = new Map<string, Highlight>()
await fetchHighlightsForArticle(
relayPool,
articleCoordinate,
article.event.id,
(highlight) => {
// Deduplicate highlights by ID as they arrive
if (!highlightsMap.has(highlight.id)) {
highlightsMap.set(highlight.id, highlight)
const highlightsList = Array.from(highlightsMap.values())
setHighlights(highlightsList.sort((a, b) => b.created_at - a.created_at))
}
// Merge streaming results with existing UI state to preserve locally created highlights
setHighlights((prev) => {
if (prev.some(h => h.id === highlight.id)) return prev
const next = [highlight, ...prev]
return next.sort((a, b) => b.created_at - a.created_at)
})
},
settings
)
console.log(`📌 Found ${highlightsMap.size} highlights`)
} catch (err) {
console.error('Failed to fetch highlights:', err)
} finally {

View File

@@ -61,6 +61,7 @@ export function useExternalUrlLoader({
[url]
)
// Load content and start streaming highlights when URL changes
useEffect(() => {
if (!relayPool || !url) return
@@ -77,7 +78,6 @@ export function useExternalUrlLoader({
const content = await fetchReadableContent(url)
setReaderContent(content)
console.log('🌐 External URL loaded:', content.title)
// Set reader loading to false immediately after content is ready
setReaderLoading(false)
@@ -88,7 +88,13 @@ export function useExternalUrlLoader({
// Seed with cached highlights first
if (cachedUrlHighlights.length > 0) {
setHighlights(cachedUrlHighlights.sort((a, b) => b.created_at - a.created_at))
setHighlights((prev) => {
// Seed with cache but keep any locally created highlights already in state
const seen = new Set<string>(cachedUrlHighlights.map(h => h.id))
const localOnly = prev.filter(h => !seen.has(h.id))
const next = [...cachedUrlHighlights, ...localOnly]
return next.sort((a, b) => b.created_at - a.created_at)
})
} else {
setHighlights([])
}
@@ -107,7 +113,7 @@ export function useExternalUrlLoader({
seen.add(highlight.id)
setHighlights((prev) => {
if (prev.some(h => h.id === highlight.id)) return prev
const next = [...prev, highlight]
const next = [highlight, ...prev]
return next.sort((a, b) => b.created_at - a.created_at)
})
},
@@ -135,6 +141,19 @@ export function useExternalUrlLoader({
}
loadExternalUrl()
}, [url, relayPool, eventStore, setSelectedUrl, setReaderContent, setReaderLoading, setIsCollapsed, setHighlights, setHighlightsLoading, setCurrentArticleCoordinate, setCurrentArticleEventId, cachedUrlHighlights])
}, [url, relayPool, eventStore, setSelectedUrl, setReaderContent, setReaderLoading, setIsCollapsed, setHighlights, setHighlightsLoading, setCurrentArticleCoordinate, setCurrentArticleEventId])
// Keep UI highlights synced with cached store updates without reloading content
useEffect(() => {
if (!url) return
if (cachedUrlHighlights.length === 0) return
setHighlights((prev) => {
const seen = new Set<string>(prev.map(h => h.id))
const additions = cachedUrlHighlights.filter(h => !seen.has(h.id))
if (additions.length === 0) return prev
const next = [...additions, ...prev]
return next.sort((a, b) => b.created_at - a.created_at)
})
}, [cachedUrlHighlights, url, setHighlights])
}

View File

@@ -60,7 +60,6 @@ export const useHighlightCreation = ({
? currentArticle.content
: readerContent?.markdown || readerContent?.html
console.log('🎯 Creating highlight...', { text: text.substring(0, 50) + '...' })
const newHighlight = await createHighlight(
text,
@@ -73,12 +72,7 @@ export const useHighlightCreation = ({
settings
)
console.log('✅ Highlight created successfully!', {
id: newHighlight.id,
isLocalOnly: newHighlight.isLocalOnly,
isOfflineCreated: newHighlight.isOfflineCreated,
publishedRelays: newHighlight.publishedRelays
})
// Highlight created successfully
// Clear the browser's text selection immediately to allow DOM update
const selection = window.getSelection()

View File

@@ -32,14 +32,7 @@ export const useHighlightedContent = ({
}: UseHighlightedContentParams) => {
// Filter highlights by URL and visibility settings
const relevantHighlights = useMemo(() => {
console.log('🔍 ContentPanel: Processing highlights', {
totalHighlights: highlights.length,
selectedUrl,
showHighlights
})
const urlFiltered = filterHighlightsByUrl(highlights, selectedUrl)
console.log('📌 URL filtered highlights:', urlFiltered.length)
// Apply visibility filtering
const classified = classifyHighlights(urlFiltered, currentUserPubkey, followedPubkeys)
@@ -49,37 +42,25 @@ export const useHighlightedContent = ({
return highlightVisibility.nostrverse
})
console.log('✅ Relevant highlights after filtering:', filtered.length, filtered.map(h => h.content.substring(0, 30)))
return filtered
}, [selectedUrl, highlights, highlightVisibility, currentUserPubkey, followedPubkeys, showHighlights])
}, [selectedUrl, highlights, highlightVisibility, currentUserPubkey, followedPubkeys])
// Prepare the final HTML with highlights applied
const finalHtml = useMemo(() => {
const sourceHtml = markdown ? renderedMarkdownHtml : html
console.log('🎨 Preparing final HTML:', {
hasMarkdown: !!markdown,
hasHtml: !!html,
renderedHtmlLength: renderedMarkdownHtml.length,
sourceHtmlLength: sourceHtml?.length || 0,
showHighlights,
relevantHighlightsCount: relevantHighlights.length
})
// Prepare final HTML
if (!sourceHtml) {
console.warn('⚠️ No source HTML available')
return ''
}
if (showHighlights && relevantHighlights.length > 0) {
console.log('✨ Applying', relevantHighlights.length, 'highlights to HTML')
const highlightedHtml = applyHighlightsToHTML(sourceHtml, relevantHighlights, highlightStyle)
console.log('✅ Highlights applied, result length:', highlightedHtml.length)
return highlightedHtml
}
console.log('📄 Returning source HTML without highlights')
return sourceHtml
}, [html, renderedMarkdownHtml, markdown, relevantHighlights, showHighlights, highlightStyle])
return { finalHtml, relevantHighlights }

View File

@@ -43,7 +43,6 @@ export const useMarkdownToHTML = (
// Replace nostr URIs with resolved titles
processed = replaceNostrUrisInMarkdownWithTitles(markdown, articleTitles)
console.log(`📚 Resolved ${articleTitles.size} article titles`)
} catch (error) {
console.warn('Failed to fetch article titles:', error)
// Fall back to basic replacement
@@ -58,12 +57,10 @@ export const useMarkdownToHTML = (
setProcessedMarkdown(processed)
console.log('📝 Converting markdown to HTML...')
const rafId = requestAnimationFrame(() => {
if (previewRef.current && !isCancelled) {
const html = previewRef.current.innerHTML
console.log('✅ Markdown converted to HTML:', html.length, 'chars')
setRenderedHtml(html)
} else if (!isCancelled) {
console.warn('⚠️ markdownPreviewRef.current is null')

View File

@@ -50,16 +50,10 @@ export function useOfflineSync({
const isNowOnline = hasRemoteRelays
if (wasLocalOnly && isNowOnline) {
console.log('✈️ Detected transition: Flight Mode → Online')
console.log('📊 Relay state:', {
connectedRelays: connectedRelays.length,
remoteRelays: connectedRelays.filter(r => !isLocalRelay(r.url)).length,
localRelays: connectedRelays.filter(r => isLocalRelay(r.url)).length
})
// Coming back online, sync events
// Wait a moment for relays to fully establish connections
setTimeout(() => {
console.log('🚀 Starting sync after delay...')
syncLocalEventsToRemote(relayPool, eventStore)
}, 2000)
}

View File

@@ -5,12 +5,10 @@ export function useOnlineStatus() {
useEffect(() => {
const handleOnline = () => {
console.log('🌐 Back online')
setIsOnline(true)
}
const handleOffline = () => {
console.log('📴 Gone offline')
setIsOnline(false)
}

View File

@@ -51,12 +51,10 @@ export function usePWAInstall() {
const choiceResult = await deferredPrompt.userChoice
if (choiceResult.outcome === 'accepted') {
console.log('✅ PWA installed')
setIsInstallable(false)
setDeferredPrompt(null)
return true
} else {
console.log('❌ PWA installation dismissed')
return false
}
} catch (error) {

View File

@@ -32,7 +32,6 @@ export const useReadingPosition = ({
// Debounced save function
const scheduleSave = useCallback((currentPosition: number) => {
if (!syncEnabled || !onSave) {
console.log('[progress] ⏭️ scheduleSave skipped:', { syncEnabled, hasOnSave: !!onSave, position: Math.round(currentPosition * 100) + '%' })
return
}
@@ -43,12 +42,7 @@ export const useReadingPosition = ({
const isInitialSave = !hasSavedOnce.current
if (!hasSignificantChange && !hasReachedCompletion && !isInitialSave) {
console.log('[progress] ⏭️ No significant change:', {
current: Math.round(currentPosition * 100) + '%',
last: Math.round(lastSavedPosition.current * 100) + '%',
diff: Math.abs(currentPosition - lastSavedPosition.current),
isInitialSave
})
// Not significant enough to save
return
}
@@ -58,9 +52,7 @@ export const useReadingPosition = ({
}
// Schedule new save
console.log('[progress] ⏰ Scheduling save in', autoSaveInterval + 'ms for position:', Math.round(currentPosition * 100) + '%')
saveTimerRef.current = setTimeout(() => {
console.log('[progress] 💾 Auto-saving position:', Math.round(currentPosition * 100) + '%')
lastSavedPosition.current = currentPosition
hasSavedOnce.current = true
onSave(currentPosition)
@@ -78,7 +70,6 @@ export const useReadingPosition = ({
}
// Always allow immediate save (including 0%)
console.log('[progress] 💾 Immediate save triggered for position:', Math.round(position * 100) + '%')
lastSavedPosition.current = position
hasSavedOnce.current = true
onSave(position)
@@ -109,11 +100,7 @@ export const useReadingPosition = ({
const prevPercent = Math.floor(position * 20) // Groups by 5%
const newPercent = Math.floor(clampedProgress * 20)
if (prevPercent !== newPercent) {
console.log('[progress] 📏 useReadingPosition:', Math.round(clampedProgress * 100) + '%', {
scrollTop,
documentHeight,
isAtBottom
})
// Position threshold crossed
}
setPosition(clampedProgress)
@@ -131,12 +118,10 @@ export const useReadingPosition = ({
if (!hasTriggeredComplete.current && position === 1) {
setIsReadingComplete(true)
hasTriggeredComplete.current = true
console.log('[progress] ✅ Completion hold satisfied (100% for', completionHoldMs, 'ms)')
onReadingComplete?.()
}
completionTimerRef.current = null
}, completionHoldMs)
console.log('[progress] ⏳ Completion hold started (waiting', completionHoldMs, 'ms)')
}
} else {
// If we moved off 100%, cancel any pending completion hold
@@ -147,7 +132,6 @@ export const useReadingPosition = ({
if (clampedProgress >= readingCompleteThreshold) {
setIsReadingComplete(true)
hasTriggeredComplete.current = true
console.log('[progress] ✅ Completion via threshold:', readingCompleteThreshold)
onReadingComplete?.()
}
}

View File

@@ -48,7 +48,6 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U
const root = document.documentElement.style
const fontKey = settings.readingFont || 'system'
console.log('🎨 Applying settings styles:', { fontKey, fontSize: settings.fontSize, theme: settings.theme })
// Apply theme with color variants (defaults to 'system' if not set)
applyTheme(
@@ -59,9 +58,7 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U
// Load font first and wait for it to be ready
if (fontKey !== 'system') {
console.log('⏳ Waiting for font to load...')
await loadFont(fontKey)
console.log('✅ Font loaded, applying styles')
}
// Apply font settings after font is loaded
@@ -76,7 +73,6 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U
// Set paragraph alignment
root.setProperty('--paragraph-alignment', settings.paragraphAlignment || 'justify')
console.log('✅ All styles applied')
}
applyStyles()

View File

@@ -11,7 +11,6 @@ if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js', { type: 'module' })
.then(registration => {
console.log('✅ Service Worker registered:', registration.scope)
// Check for updates periodically
setInterval(() => {
@@ -25,7 +24,6 @@ if ('serviceWorker' in navigator) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New service worker available
console.log('🔄 New version available! Reload to update.')
// Optionally show a toast notification
const updateAvailable = new CustomEvent('sw-update-available')

View File

@@ -48,7 +48,6 @@ function getFromCache(naddr: string): ArticleContent | null {
return null
}
console.log('📦 Loaded article from cache:', naddr)
return content
} catch {
return null
@@ -63,7 +62,6 @@ function saveToCache(naddr: string, content: ArticleContent): void {
timestamp: Date.now()
}
localStorage.setItem(cacheKey, JSON.stringify(cached))
console.log('💾 Saved article to cache:', naddr)
} catch (err) {
console.warn('Failed to cache article:', err)
// Silently fail if storage is full or unavailable

View File

@@ -30,8 +30,8 @@ async function decryptEvent(
} catch {
try {
await Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner, 'nip44' as UnlockMode)
} catch (err) {
console.log("[bunker] ❌ nip44.decrypt failed:", err instanceof Error ? err.message : String(err))
} catch (_err) {
// Ignore unlock errors
}
}
} else if (evt.content && evt.content.length > 0) {
@@ -45,8 +45,8 @@ async function decryptEvent(
if (looksLikeNip44 && hasNip44Decrypt(signerCandidate)) {
try {
decryptedContent = await (signerCandidate as { nip44: { decrypt: DecryptFn } }).nip44.decrypt(evt.pubkey, evt.content)
} catch (err) {
console.log("[bunker] ❌ nip44.decrypt failed:", err instanceof Error ? err.message : String(err))
} catch (_err) {
// Ignore NIP-44 decryption errors
}
}
@@ -54,8 +54,8 @@ async function decryptEvent(
if (!decryptedContent && hasNip04Decrypt(signerCandidate)) {
try {
decryptedContent = await (signerCandidate as { nip04: { decrypt: DecryptFn } }).nip04.decrypt(evt.pubkey, evt.content)
} catch (err) {
console.log("[bunker] ❌ nip04.decrypt failed:", err instanceof Error ? err.message : String(err))
} catch (_err) {
// Ignore NIP-04 decryption errors
}
}

View File

@@ -15,7 +15,6 @@ export const fetchContacts = async (
): Promise<Set<string>> => {
try {
const relayUrls = prioritizeLocalRelays(Array.from(relayPool.relays.values()).map(relay => relay.url))
console.log('🔍 Fetching contacts (kind 3) for user:', pubkey)
const partialFollowed = new Set<string>()
const events = await queryEvents(
@@ -51,9 +50,7 @@ export const fetchContacts = async (
}
// merged already via streams
console.log('📊 Contact events fetched:', events.length)
console.log('👥 Followed contacts:', followed.size)
return followed
} catch (error) {
console.error('Failed to fetch contacts:', error)

View File

@@ -73,13 +73,11 @@ class ContactsController {
// Skip if already loaded for this pubkey (unless forced)
if (!force && this.isLoadedFor(pubkey)) {
console.log('[contacts] ✅ Already loaded for', pubkey.slice(0, 8))
this.emitContacts(this.currentContacts)
return
}
this.setLoading(true)
console.log('[contacts] 🔍 Loading contacts for', pubkey.slice(0, 8))
try {
const contacts = await fetchContacts(
@@ -89,7 +87,6 @@ class ContactsController {
// Stream partial updates
this.currentContacts = new Set(partial)
this.emitContacts(this.currentContacts)
console.log('[contacts] 📥 Partial contacts:', partial.size)
}
)
@@ -98,7 +95,6 @@ class ContactsController {
this.lastLoadedPubkey = pubkey
this.emitContacts(this.currentContacts)
console.log('[contacts] ✅ Loaded', contacts.size, 'contacts')
} catch (error) {
console.error('[contacts] ❌ Failed to load contacts:', error)
this.currentContacts.clear()

View File

@@ -36,12 +36,10 @@ export async function createDeletionRequest(
const signed = await factory.sign(draft)
console.log('🗑️ Created kind:5 deletion request for event:', eventId.slice(0, 8))
// Publish to relays
await relayPool.publish(RELAYS, signed)
console.log('✅ Deletion request published to', RELAYS.length, 'relay(s)')
return signed
}

View File

@@ -33,11 +33,9 @@ export const fetchBlogPostsFromAuthors = async (
): Promise<BlogPostPreview[]> => {
try {
if (pubkeys.length === 0) {
console.log('⚠️ No pubkeys to fetch blog posts from')
return []
}
console.log('📚 Fetching blog posts (kind 30023) from', pubkeys.length, 'authors', limit ? `(limit: ${limit})` : '(no limit)')
// Deduplicate replaceable events by keeping the most recent version
// Group by author + d-tag identifier
@@ -75,7 +73,6 @@ export const fetchBlogPostsFromAuthors = async (
}
)
console.log('📊 Blog post events fetched (unique):', uniqueEvents.size)
// Convert to blog post previews and sort by published date (most recent first)
const blogPosts: BlogPostPreview[] = Array.from(uniqueEvents.values())
@@ -97,7 +94,6 @@ export const fetchBlogPostsFromAuthors = async (
return timeB - timeA // Most recent first
})
console.log('📰 Processed', blogPosts.length, 'unique blog posts')
return blogPosts
} catch (error) {

View File

@@ -46,7 +46,6 @@ export async function createHighlight(
}
// Create EventFactory with the account as signer
console.log("[bunker] Creating EventFactory with signer:", { signerType: account.signer?.constructor?.name })
const factory = new EventFactory({ signer: account.signer })
let blueprintSource: NostrEvent | AddressPointer | string
@@ -117,9 +116,7 @@ export async function createHighlight(
}
// Sign the event
console.log('[bunker] Signing highlight event...', { kind: highlightEvent.kind, tags: highlightEvent.tags.length })
const signedEvent = await factory.sign(highlightEvent)
console.log('[bunker] ✅ Highlight signed successfully!', { id: signedEvent.id.slice(0, 8) })
// Use unified write service to store and publish
await publishEvent(relayPool, eventStore, signedEvent)

View File

@@ -22,7 +22,6 @@ export const fetchHighlights = async (
const cacheKey = highlightCache.authorKey(pubkey)
const cached = highlightCache.get(cacheKey)
if (cached) {
console.log(`📌 Using cached highlights for author (${cached.length} items)`)
// Stream cached highlights if callback provided
if (onHighlight) {
cached.forEach(h => onHighlight(h))
@@ -50,7 +49,6 @@ export const fetchHighlights = async (
}
)
console.log(`📌 Fetched ${rawEvents.length} highlight events for author:`, pubkey.slice(0, 8))
// Store all events in event store if provided
if (eventStore) {

View File

@@ -23,7 +23,6 @@ export const fetchHighlightsForArticle = async (
const cacheKey = highlightCache.articleKey(articleCoordinate)
const cached = highlightCache.get(cacheKey)
if (cached) {
console.log(`📌 Using cached highlights for article (${cached.length} items)`)
// Stream cached highlights if callback provided
if (onHighlight) {
cached.forEach(h => onHighlight(h))
@@ -54,7 +53,6 @@ export const fetchHighlightsForArticle = async (
])
const rawEvents = [...aTagEvents, ...eTagEvents]
console.log(`📌 Fetched ${rawEvents.length} highlight events for article:`, articleCoordinate)
// Store all events in event store if provided
if (eventStore) {

View File

@@ -22,7 +22,6 @@ export const fetchHighlightsForUrl = async (
const cacheKey = highlightCache.urlKey(url)
const cached = highlightCache.get(cacheKey)
if (cached) {
console.log(`📌 Using cached highlights for URL (${cached.length} items)`)
// Stream cached highlights if callback provided
if (onHighlight) {
cached.forEach(h => onHighlight(h))
@@ -50,7 +49,6 @@ export const fetchHighlightsForUrl = async (
}
)
console.log(`📌 Fetched ${rawEvents.length} highlight events for URL:`, url)
// Store all events in event store if provided
if (eventStore) {

View File

@@ -21,11 +21,9 @@ export const fetchHighlightsFromAuthors = async (
): Promise<Highlight[]> => {
try {
if (pubkeys.length === 0) {
console.log('⚠️ No pubkeys to fetch highlights from')
return []
}
console.log('💡 Fetching highlights (kind 9802) from', pubkeys.length, 'authors')
const seenIds = new Set<string>()
const rawEvents = await queryEvents(
@@ -55,7 +53,6 @@ export const fetchHighlightsFromAuthors = async (
const uniqueEvents = dedupeHighlights(rawEvents)
const highlights = uniqueEvents.map(eventToHighlight)
console.log('💡 Processed', highlights.length, 'unique highlights')
return sortHighlights(highlights)
} catch (error) {

View File

@@ -110,7 +110,6 @@ class HighlightsController {
// Skip if already loaded for this pubkey (unless forced)
if (!force && this.isLoadedFor(pubkey)) {
console.log('[highlights] ✅ Already loaded for', pubkey.slice(0, 8))
this.emitHighlights(this.currentHighlights)
return
}
@@ -120,7 +119,6 @@ class HighlightsController {
const currentGeneration = this.generation
this.setLoading(true)
console.log('[highlights] 🔍 Loading highlights for', pubkey.slice(0, 8))
try {
const seenIds = new Set<string>()
@@ -134,7 +132,6 @@ class HighlightsController {
}
if (lastSyncedAt) {
filter.since = lastSyncedAt
console.log('[highlights] 📅 Incremental sync since', new Date(lastSyncedAt * 1000).toISOString())
}
const events = await queryEvents(
@@ -165,7 +162,6 @@ class HighlightsController {
// Check if still active after async operation
if (currentGeneration !== this.generation) {
console.log('[highlights] ⚠️ Load cancelled (generation mismatch)')
return
}
@@ -189,7 +185,6 @@ class HighlightsController {
this.setLastSyncedAt(pubkey, newestTimestamp)
}
console.log('[highlights] ✅ Loaded', sorted.length, 'highlights')
} catch (error) {
console.error('[highlights] ❌ Failed to load highlights:', error)
this.currentHighlights = []

View File

@@ -13,7 +13,6 @@ const CACHE_NAME = 'boris-image-cache-v1'
export async function clearImageCache(): Promise<void> {
try {
await caches.delete(CACHE_NAME)
console.log('🗑️ Cleared all cached images')
} catch (err) {
console.error('Failed to clear image cache:', err)
}

View File

@@ -17,7 +17,6 @@ export async function fetchLinks(
userPubkey: string,
onItem?: (item: ReadItem) => void
): Promise<ReadItem[]> {
console.log('🔗 [Links] Fetching external links for user:', userPubkey.slice(0, 8))
const linksMap = new Map<string, ReadItem>()
@@ -37,11 +36,6 @@ export async function fetchLinks(
fetchReadArticles(relayPool, userPubkey)
])
console.log('📊 [Links] Data fetched:', {
readingProgress: progressEvents.length,
markedAsRead: markedAsReadArticles.length
})
// Process reading progress events (kind 39802)
processReadingProgress(progressEvents, linksMap)
if (onItem) {
@@ -79,7 +73,6 @@ export async function fetchLinks(
const validLinks = filterValidItems(links)
const sortedLinks = sortByReadingActivity(validLinks)
console.log('✅ [Links] Processed', sortedLinks.length, 'total links')
return sortedLinks
} catch (error) {

View File

@@ -24,7 +24,6 @@ export const fetchNostrverseBlogPosts = async (
onPost?: (post: BlogPostPreview) => void
): Promise<BlogPostPreview[]> => {
try {
console.log('[NOSTRVERSE] 📚 Fetching blog posts (kind 30023), limit:', limit)
// Deduplicate replaceable events by keeping the most recent version
const uniqueEvents = new Map<string, NostrEvent>()
@@ -63,7 +62,6 @@ export const fetchNostrverseBlogPosts = async (
}
)
console.log('[NOSTRVERSE] 📊 Blog post events fetched (unique):', uniqueEvents.size)
// Convert to blog post previews and sort by published date (most recent first)
const blogPosts: BlogPostPreview[] = Array.from(uniqueEvents.values())
@@ -81,7 +79,6 @@ export const fetchNostrverseBlogPosts = async (
return timeB - timeA // Most recent first
})
console.log('[NOSTRVERSE] 📰 Processed', blogPosts.length, 'unique blog posts')
return blogPosts
} catch (error) {
@@ -103,7 +100,6 @@ export const fetchNostrverseHighlights = async (
eventStore?: IEventStore
): Promise<Highlight[]> => {
try {
console.log('[NOSTRVERSE] 💡 Fetching highlights (kind 9802), limit:', limit)
const seenIds = new Set<string>()
// Collect but do not block callers awaiting network completion
@@ -133,7 +129,6 @@ export const fetchNostrverseHighlights = async (
const uniqueEvents = dedupeHighlights([...collected, ...rawEvents])
const highlights = uniqueEvents.map(eventToHighlight)
console.log('[NOSTRVERSE] 💡 Processed', highlights.length, 'unique highlights')
return sortHighlights(highlights)
} catch (error) {

View File

@@ -20,7 +20,6 @@ const syncStateListeners: Array<(eventId: string, isSyncing: boolean) => void> =
*/
export function markEventAsOfflineCreated(eventId: string): void {
offlineCreatedEvents.add(eventId)
console.log(`📝 Marked event ${eventId.slice(0, 8)} as offline-created. Total: ${offlineCreatedEvents.size}`)
}
/**
@@ -57,49 +56,35 @@ export async function syncLocalEventsToRemote(
eventStore: IEventStore
): Promise<void> {
if (isSyncing) {
console.log('⏳ Sync already in progress, skipping...')
return
}
console.log('🔄 Coming back online - syncing local events to remote relays...')
console.log(`📦 Offline events tracked: ${offlineCreatedEvents.size}`)
isSyncing = true
try {
const remoteRelays = RELAYS.filter(url => !isLocalRelay(url))
console.log(`📡 Remote relays: ${remoteRelays.length}`)
if (remoteRelays.length === 0) {
console.log('⚠️ No remote relays available for sync')
isSyncing = false
return
}
if (offlineCreatedEvents.size === 0) {
console.log('✅ No offline events to sync')
isSyncing = false
return
}
// Get events from EventStore using the tracked IDs
const eventsToSync: NostrEvent[] = []
console.log(`🔍 Querying EventStore for ${offlineCreatedEvents.size} offline events...`)
for (const eventId of offlineCreatedEvents) {
const event = eventStore.getEvent(eventId)
if (event) {
console.log(`📥 Found event ${eventId.slice(0, 8)} (kind ${event.kind}) in EventStore`)
eventsToSync.push(event)
} else {
console.warn(`⚠️ Event ${eventId.slice(0, 8)} not found in EventStore`)
}
}
console.log(`📊 Total events to sync: ${eventsToSync.length}`)
if (eventsToSync.length === 0) {
console.log('✅ No events found in EventStore to sync')
isSyncing = false
offlineCreatedEvents.clear()
return
@@ -110,8 +95,6 @@ export async function syncLocalEventsToRemote(
new Map(eventsToSync.map(e => [e.id, e])).values()
)
console.log(`📤 Syncing ${uniqueEvents.length} event(s) to remote relays...`)
// Mark all events as syncing
uniqueEvents.forEach(event => {
syncingEvents.add(event.id)
@@ -119,21 +102,16 @@ export async function syncLocalEventsToRemote(
})
// Publish to remote relays
let successCount = 0
const successfulIds: string[] = []
for (const event of uniqueEvents) {
try {
await relayPool.publish(remoteRelays, event)
successCount++
successfulIds.push(event.id)
console.log(`✅ Synced event ${event.id.slice(0, 8)}`)
} catch (error) {
console.warn(`⚠️ Failed to sync event ${event.id.slice(0, 8)}:`, error)
// Silently fail for individual events
}
}
console.log(`✅ Synced ${successCount}/${uniqueEvents.length} events to remote relays`)
// Clear syncing state and offline tracking for successful events
successfulIds.forEach(eventId => {
@@ -150,7 +128,7 @@ export async function syncLocalEventsToRemote(
}
})
} catch (error) {
console.error('❌ Error during offline sync:', error)
// Silently fail
} finally {
isSyncing = false
}

View File

@@ -22,7 +22,6 @@ export const fetchProfiles = async (
}
const uniquePubkeys = Array.from(new Set(pubkeys))
console.log('👤 Fetching profiles (kind:0) for', uniquePubkeys.length, 'authors')
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
const prioritized = prioritizeLocalRelays(relayUrls)
@@ -65,7 +64,6 @@ export const fetchProfiles = async (
await lastValueFrom(merge(local$, remote$).pipe(toArray()))
const profiles = Array.from(profilesByPubkey.values())
console.log('✅ Fetched', profiles.length, 'unique profiles')
// Rebroadcast profiles to local/all relays based on settings
if (profiles.length > 0) {

View File

@@ -42,12 +42,10 @@ export async function createEventReaction(
const signed = await factory.sign(draft)
console.log('📚 Created kind:7 reaction (mark as read) for event:', eventId.slice(0, 8))
// Publish to relays
await relayPool.publish(RELAYS, signed)
console.log('✅ Reaction published to', RELAYS.length, 'relay(s)')
return signed
}
@@ -94,12 +92,10 @@ export async function createWebsiteReaction(
const signed = await factory.sign(draft)
console.log('📚 Created kind:17 reaction (mark as read) for URL:', normalizedUrl)
// Publish to relays
await relayPool.publish(RELAYS, signed)
console.log('✅ Website reaction published to', RELAYS.length, 'relay(s)')
return signed
}

View File

@@ -27,31 +27,23 @@ export function processReadingProgress(
events: NostrEvent[],
readsMap: Map<string, ReadItem>
): void {
console.log('[progress] 🔧 processReadingProgress called with', events.length, 'events')
for (const event of events) {
if (event.kind !== READING_PROGRESS_KIND) {
console.log('[progress] ⏭️ Skipping event with wrong kind:', event.kind)
continue
}
const dTag = event.tags.find(t => t[0] === 'd')?.[1]
if (!dTag) {
console.log('[progress] ⚠️ Event missing d-tag:', event.id.slice(0, 8))
continue
}
console.log('[progress] 📝 Processing event:', event.id.slice(0, 8), 'd-tag:', dTag.slice(0, 50))
try {
const content = JSON.parse(event.content)
const position = content.progress || 0
console.log('[progress] 📊 Progress value:', position, '(' + Math.round(position * 100) + '%)')
// Validate progress is between 0 and 1 (NIP-85 requirement)
if (position < 0 || position > 1) {
console.warn('[progress] ❌ Invalid progress value (must be 0-1):', position, 'event:', event.id.slice(0, 8))
continue
}
@@ -76,13 +68,10 @@ export function processReadingProgress(
})
itemId = naddr
itemType = 'article'
console.log('[progress] ✅ Converted coordinate to naddr:', naddr.slice(0, 50))
} catch (e) {
console.warn('[progress] ❌ Failed to encode naddr from coordinate:', dTag)
continue
}
} else {
console.warn('[progress] ⚠️ Invalid coordinate format:', dTag)
continue
}
} else if (dTag.startsWith('url:')) {
@@ -92,13 +81,10 @@ export function processReadingProgress(
itemUrl = atob(encoded.replace(/-/g, '+').replace(/_/g, '/'))
itemId = itemUrl
itemType = 'external'
console.log('[progress] ✅ Decoded URL:', itemUrl.slice(0, 50))
} catch (e) {
console.warn('[progress] ❌ Failed to decode URL from d tag:', dTag)
continue
}
} else {
console.warn('[progress] ⚠️ Unknown d-tag format:', dTag)
continue
}
@@ -114,16 +100,11 @@ export function processReadingProgress(
readingProgress: position,
readingTimestamp: timestamp
})
console.log('[progress] ✅ Added/updated item in readsMap:', itemId.slice(0, 50), '=', Math.round(position * 100) + '%')
} else {
console.log('[progress] ⏭️ Skipping older event for:', itemId.slice(0, 50))
}
} catch (error) {
console.warn('[progress] ❌ Failed to parse reading progress event:', error)
// Silently fail
}
}
console.log('[progress] 🏁 processReadingProgress finished, readsMap size:', readsMap.size)
}
/**

View File

@@ -49,14 +49,10 @@ function generateDTag(naddrOrUrl: string): string {
const decoded = nip19.decode(naddrOrUrl)
if (decoded.type === 'naddr') {
const dTag = `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier || ''}`
console.log('[progress] 📋 Generated d-tag from naddr:', {
naddr: naddrOrUrl.slice(0, 50) + '...',
dTag: dTag.slice(0, 80) + '...'
})
return dTag
}
} catch (e) {
console.warn('Failed to decode naddr:', naddrOrUrl)
// Ignore decode errors
}
}
@@ -119,14 +115,6 @@ export async function saveReadingPosition(
articleIdentifier: string,
position: ReadingPosition
): Promise<void> {
console.log('[progress] 💾 saveReadingPosition: Starting save:', {
identifier: articleIdentifier.slice(0, 50) + '...',
position: position.position,
positionPercent: Math.round(position.position * 100) + '%',
timestamp: position.timestamp,
scrollTop: position.scrollTop
})
const now = Math.floor(Date.now() / 1000)
const progressContent: ReadingProgressContent = {
@@ -138,13 +126,6 @@ export async function saveReadingPosition(
const tags = generateProgressTags(articleIdentifier)
console.log('[progress] 📝 Creating event with:', {
kind: READING_PROGRESS_KIND,
content: progressContent,
tags: tags.map(t => `[${t.join(', ')}]`).join(', '),
created_at: now
})
const draft = await factory.create(async () => ({
kind: READING_PROGRESS_KIND,
content: JSON.stringify(progressContent),
@@ -152,20 +133,9 @@ export async function saveReadingPosition(
created_at: now
}))
console.log('[progress] ✍️ Signing event...')
const signed = await factory.sign(draft)
console.log('[progress] 📡 Publishing event:', {
id: signed.id,
kind: signed.kind,
pubkey: signed.pubkey.slice(0, 8) + '...',
content: signed.content,
tags: signed.tags
})
await publishEvent(relayPool, eventStore, signed)
console.log('[progress] ✅ Event published successfully, ID:', signed.id.slice(0, 16))
}
/**
@@ -179,12 +149,6 @@ export async function loadReadingPosition(
): Promise<ReadingPosition | null> {
const dTag = generateDTag(articleIdentifier)
console.log('📖 [ReadingProgress] Loading position:', {
pubkey: pubkey.slice(0, 8) + '...',
identifier: articleIdentifier.slice(0, 32) + '...',
dTag: dTag.slice(0, 50) + '...'
})
// Check local event store first
try {
const localEvent = await firstValueFrom(
@@ -193,12 +157,6 @@ export async function loadReadingPosition(
if (localEvent) {
const content = getReadingProgressContent(localEvent)
if (content) {
console.log('✅ [ReadingProgress] Loaded from local store:', {
position: content.position,
positionPercent: Math.round(content.position * 100) + '%',
timestamp: content.timestamp
})
// Fetch from relays in background to get any updates
relayPool
.subscription(RELAYS, {
@@ -213,7 +171,7 @@ export async function loadReadingPosition(
}
}
} catch (err) {
console.log('📭 No cached reading progress found, fetching from relays...')
// Ignore errors and fetch from relays
}
// Fetch from relays
@@ -226,13 +184,7 @@ export async function loadReadingPosition(
getReadingProgressContent
)
if (result) {
console.log('✅ [ReadingProgress] Loaded from relays')
return result
}
console.log('📭 No reading progress found')
return null
return result || null
}
// Helper function to fetch from relays with timeout

View File

@@ -1,11 +1,15 @@
import { RelayPool } from 'applesauce-relay'
import { IEventStore } from 'applesauce-core'
import { Filter, NostrEvent } from 'nostr-tools'
import { NostrEvent } from 'nostr-tools'
import { queryEvents } from './dataFetch'
import { KINDS } from '../config/kinds'
import { RELAYS } from '../config/relays'
import { processReadingProgress } from './readingDataProcessor'
import { ReadItem } from './readsService'
import { MARK_AS_READ_EMOJI } from './reactionService'
import { nip19 } from 'nostr-tools'
console.log('[readingProgress] Module loaded')
type ProgressMapCallback = (progressMap: Map<string, number>) => void
type LoadingCallback = (loading: boolean) => void
@@ -20,11 +24,14 @@ const PROGRESS_CACHE_KEY = 'reading_progress_cache_v1'
class ReadingProgressController {
private progressListeners: ProgressMapCallback[] = []
private loadingListeners: LoadingCallback[] = []
private markedAsReadListeners: (() => void)[] = []
private currentProgressMap: Map<string, number> = new Map()
private markedAsReadIds: Set<string> = new Set()
private lastLoadedPubkey: string | null = null
private generation = 0
private timelineSubscription: { unsubscribe: () => void } | null = null
private isLoading = false
onProgress(cb: ProgressMapCallback): () => void {
this.progressListeners.push(cb)
@@ -40,15 +47,25 @@ class ReadingProgressController {
}
}
onMarkedAsReadChanged(cb: () => void): () => void {
this.markedAsReadListeners.push(cb)
return () => {
this.markedAsReadListeners = this.markedAsReadListeners.filter(l => l !== cb)
}
}
private setLoading(loading: boolean): void {
this.loadingListeners.forEach(cb => cb(loading))
}
private emitProgress(progressMap: Map<string, number>): void {
console.log('[progress] 📡 Emitting to', this.progressListeners.length, 'listeners with', progressMap.size, 'items')
this.progressListeners.forEach(cb => cb(new Map(progressMap)))
}
private emitMarkedAsReadChanged(): void {
this.markedAsReadListeners.forEach(cb => cb())
}
/**
* Get current reading progress map without triggering a reload
*/
@@ -81,7 +98,7 @@ class ReadingProgressController {
parsed[pubkey] = Object.fromEntries(progressMap.entries())
localStorage.setItem(PROGRESS_CACHE_KEY, JSON.stringify(parsed))
} catch (err) {
console.warn('[progress] ⚠️ Failed to persist reading progress cache:', err)
// Silently fail cache persistence
}
}
@@ -92,6 +109,20 @@ class ReadingProgressController {
return this.currentProgressMap.get(naddr)
}
/**
* Check if article is marked as read
*/
isMarkedAsRead(naddr: string): boolean {
return this.markedAsReadIds.has(naddr)
}
/**
* Get all marked as read IDs (for debugging)
*/
getMarkedAsReadIds(): string[] {
return Array.from(this.markedAsReadIds)
}
/**
* Check if reading progress is loaded for a specific pubkey
*/
@@ -109,29 +140,16 @@ class ReadingProgressController {
try {
this.timelineSubscription.unsubscribe()
} catch (err) {
console.warn('[progress] ⚠️ Failed to unsubscribe timeline on reset:', err)
// Silently fail on unsubscribe
}
this.timelineSubscription = null
}
this.currentProgressMap = new Map()
this.markedAsReadIds = new Set()
this.lastLoadedPubkey = null
this.emitProgress(this.currentProgressMap)
}
/**
* Get last synced timestamp for incremental loading
*/
private getLastSyncedAt(pubkey: string): number | null {
try {
const data = localStorage.getItem(LAST_SYNCED_KEY)
if (!data) return null
const parsed = JSON.parse(data)
return parsed[pubkey] || null
} catch {
return null
}
}
/**
* Update last synced timestamp
*/
@@ -142,7 +160,7 @@ class ReadingProgressController {
parsed[pubkey] = timestamp
localStorage.setItem(LAST_SYNCED_KEY, JSON.stringify(parsed))
} catch (err) {
console.warn('Failed to update last synced timestamp:', err)
// Silently fail
}
}
@@ -158,94 +176,98 @@ class ReadingProgressController {
const { relayPool, eventStore, pubkey, force = false } = params
const startGeneration = this.generation
console.log('[readingProgress] start() called for pubkey:', pubkey.slice(0, 16), '...', 'force:', force)
// Skip if already loaded for this pubkey and not forcing
if (!force && this.isLoadedFor(pubkey)) {
console.log('📊 [ReadingProgress] Already loaded for', pubkey.slice(0, 8))
console.log('[readingProgress] Already loaded for pubkey, skipping')
return
}
// Prevent concurrent starts
if (this.isLoading) {
console.log('[readingProgress] Already loading, skipping concurrent start')
return
}
console.log('📊 [ReadingProgress] Loading for', pubkey.slice(0, 8), force ? '(forced)' : '')
this.setLoading(true)
this.lastLoadedPubkey = pubkey
this.isLoading = true
try {
// Seed from local cache immediately (survives refresh/flight mode)
const cached = this.loadCachedProgress(pubkey)
if (cached.size > 0) {
console.log('📊 [ReadingProgress] Seeded from cache:', cached.size, 'items')
this.currentProgressMap = cached
this.emitProgress(this.currentProgressMap)
}
// Subscribe to local timeline for immediate and reactive updates
// Clean up any previous subscription first
// Subscribe to local eventStore timeline for immediate and reactive updates
// This handles both local writes and synced events from relays
if (this.timelineSubscription) {
try {
this.timelineSubscription.unsubscribe()
} catch (err) {
console.warn('[progress] ⚠️ Failed to unsubscribe previous timeline:', err)
// Silently fail
}
this.timelineSubscription = null
}
console.log('[readingProgress] Setting up eventStore subscription...')
const timeline$ = eventStore.timeline({
kinds: [KINDS.ReadingProgress],
authors: [pubkey]
})
const generationAtSubscribe = this.generation
this.timelineSubscription = timeline$.subscribe((localEvents: NostrEvent[]) => {
// Ignore if controller generation has changed (e.g., logout/login)
if (generationAtSubscribe !== this.generation) return
if (!Array.isArray(localEvents) || localEvents.length === 0) return
console.log('📊 [ReadingProgress] Timeline update with', localEvents.length, 'event(s)')
this.processEvents(localEvents)
})
console.log('[readingProgress] EventStore subscription ready - updates streaming')
// Query events from relays
// Force full sync if map is empty (first load) or if explicitly forced
const needsFullSync = force || this.currentProgressMap.size === 0
const lastSynced = needsFullSync ? null : this.getLastSyncedAt(pubkey)
const filter: Filter = {
// Mark as loaded immediately - queries run in background non-blocking
this.lastLoadedPubkey = pubkey
// Query reading progress from relays in background (non-blocking, fire-and-forget)
console.log('[readingProgress] Starting background relay query for reading progress...')
queryEvents(relayPool, {
kinds: [KINDS.ReadingProgress],
authors: [pubkey]
}
if (lastSynced && !needsFullSync) {
filter.since = lastSynced
console.log('📊 [ReadingProgress] Incremental sync since', new Date(lastSynced * 1000).toISOString())
} else {
console.log('📊 [ReadingProgress] Full sync (map size:', this.currentProgressMap.size + ')')
}
}, { relayUrls: RELAYS })
.then((relayEvents) => {
if (startGeneration !== this.generation) return
console.log('[readingProgress] Got reading progress from relays:', relayEvents.length)
if (relayEvents.length > 0) {
relayEvents.forEach(e => eventStore.add(e))
this.processEvents(relayEvents)
const now = Math.floor(Date.now() / 1000)
this.updateLastSyncedAt(pubkey, now)
}
})
.catch((err) => {
console.warn('[readingProgress] Background reading progress query failed:', err)
})
const relayEvents = await queryEvents(relayPool, filter, { relayUrls: RELAYS })
if (startGeneration !== this.generation) {
console.log('📊 [ReadingProgress] Cancelled (generation changed)')
return
}
// Load mark-as-read reactions in background (non-blocking, streaming)
console.log('[readingProgress] Starting background relay query for mark-as-read reactions...')
this.loadMarkAsReadReactions(relayPool, eventStore, pubkey, startGeneration)
.then(() => {
console.log('[readingProgress] Mark-as-read reactions loading complete')
})
.catch((err) => {
console.warn('[readingProgress] Mark-as-read reactions loading failed:', err)
})
if (relayEvents.length > 0) {
// Add to event store
relayEvents.forEach(e => eventStore.add(e))
// Process and emit (merge with existing)
this.processEvents(relayEvents)
console.log('📊 [ReadingProgress] Loaded', relayEvents.length, 'events from relays')
// Update last synced
const now = Math.floor(Date.now() / 1000)
this.updateLastSyncedAt(pubkey, now)
} else {
console.log('📊 [ReadingProgress] No new events from relays')
}
} catch (err) {
console.error('📊 [ReadingProgress] Failed to load:', err)
console.error('📊 [ReadingProgress] Failed to setup:', err)
} finally {
if (startGeneration === this.generation) {
this.setLoading(false)
}
this.isLoading = false
console.log('[readingProgress] === LOADED ===')
console.log('[readingProgress] progressMap keys:', Array.from(this.currentProgressMap.keys()))
console.log('[readingProgress] markedAsReadIds:', Array.from(this.markedAsReadIds))
}
}
@@ -253,8 +275,6 @@ class ReadingProgressController {
* Process events and update progress map
*/
private processEvents(events: NostrEvent[]): void {
console.log('[progress] 🔄 Processing', events.length, 'events')
const readsMap = new Map<string, ReadItem>()
// Merge with existing progress
@@ -267,24 +287,17 @@ class ReadingProgressController {
})
}
console.log('[progress] 📦 Starting with', readsMap.size, 'existing items')
// Process new events
processReadingProgress(events, readsMap)
console.log('[progress] 📦 After processing:', readsMap.size, 'items')
// Convert back to progress map (naddr -> progress)
const newProgressMap = new Map<string, number>()
for (const [id, item] of readsMap.entries()) {
if (item.readingProgress !== undefined && item.type === 'article') {
newProgressMap.set(id, item.readingProgress)
console.log('[progress] ✅ Added:', id.slice(0, 50) + '...', '=', Math.round(item.readingProgress * 100) + '%')
}
}
console.log('[progress] 📊 Final progress map size:', newProgressMap.size)
this.currentProgressMap = newProgressMap
this.emitProgress(this.currentProgressMap)
@@ -293,6 +306,84 @@ class ReadingProgressController {
this.persistProgress(this.lastLoadedPubkey, this.currentProgressMap)
}
}
/**
* Load mark-as-read reactions in background (non-blocking)
*/
private async loadMarkAsReadReactions(
relayPool: RelayPool,
_eventStore: IEventStore,
pubkey: string,
generation: number
): Promise<void> {
try {
// Stream kind:17 (URL reactions) and kind:7 (event reactions) in parallel
console.log('[readingProgress] Querying kind:17 and kind:7 reactions (streaming)...')
const seenReactionIds = new Set<string>()
const handleUrlReaction = (evt: NostrEvent) => {
if (seenReactionIds.has(evt.id)) return
seenReactionIds.add(evt.id)
if (evt.content !== MARK_AS_READ_EMOJI) return
const rTag = evt.tags.find(t => t[0] === 'r')?.[1]
if (!rTag) return
this.markedAsReadIds.add(rTag)
this.emitMarkedAsReadChanged()
}
const pendingEventIds = new Set<string>()
const handleEventReaction = (evt: NostrEvent) => {
if (seenReactionIds.has(evt.id)) return
seenReactionIds.add(evt.id)
if (evt.content !== MARK_AS_READ_EMOJI) return
const eTag = evt.tags.find(t => t[0] === 'e')?.[1]
if (!eTag) return
pendingEventIds.add(eTag)
}
// Fire queries with onEvent callbacks for streaming behavior
const [kind17Events, kind7Events] = await Promise.all([
queryEvents(relayPool, { kinds: [17], authors: [pubkey] }, { relayUrls: RELAYS, onEvent: handleUrlReaction }),
queryEvents(relayPool, { kinds: [7], authors: [pubkey] }, { relayUrls: RELAYS, onEvent: handleEventReaction })
])
if (generation !== this.generation) return
// Include any reactions that arrived only at EOSE
kind17Events.forEach(handleUrlReaction)
kind7Events.forEach(handleEventReaction)
if (pendingEventIds.size > 0) {
// Fetch referenced 30023 events, streaming not required here
const ids = Array.from(pendingEventIds)
const articleEvents = await queryEvents(relayPool, { kinds: [KINDS.BlogPost], ids }, { relayUrls: RELAYS })
const eventIdToNaddr = new Map<string, string>()
for (const article of articleEvents) {
const dTag = article.tags.find(t => t[0] === 'd')?.[1]
if (!dTag) continue
try {
const naddr = nip19.naddrEncode({ kind: KINDS.BlogPost, pubkey: article.pubkey, identifier: dTag })
eventIdToNaddr.set(article.id, naddr)
} catch (e) {
console.warn('[readingProgress] Failed to encode naddr for article:', article.id)
}
}
// Map pending event IDs to naddrs and emit
for (const eId of pendingEventIds) {
const naddr = eventIdToNaddr.get(eId)
if (naddr) {
this.markedAsReadIds.add(naddr)
}
}
this.emitMarkedAsReadChanged()
}
console.log('[readingProgress] Mark-as-read reactions complete. Total:', Array.from(this.markedAsReadIds).length)
} catch (err) {
console.warn('[readingProgress] Failed to load mark-as-read reactions:', err)
}
}
}
export const readingProgressController = new ReadingProgressController()

View File

@@ -46,7 +46,6 @@ export async function fetchAllReads(
bookmarks: Bookmark[],
onItem?: (item: ReadItem) => void
): Promise<ReadItem[]> {
console.log('📚 [Reads] Fetching all reads for user:', userPubkey.slice(0, 8))
const readsMap = new Map<string, ReadItem>()
@@ -66,25 +65,14 @@ export async function fetchAllReads(
fetchReadArticles(relayPool, userPubkey)
])
console.log('📊 [Reads] Data fetched:', {
readingProgress: progressEvents.length,
markedAsRead: markedAsReadArticles.length,
bookmarks: bookmarks.length
})
// Process reading progress events (kind 39802)
processReadingProgress(progressEvents, readsMap)
if (onItem) {
readsMap.forEach(item => {
if (item.type === 'article') onItem(item)
})
}
// Process marked-as-read and emit items
processMarkedAsRead(markedAsReadArticles, readsMap)
if (onItem) {
readsMap.forEach(item => {
if (item.type === 'article') onItem(item)
onItem(item)
})
}
@@ -120,7 +108,6 @@ export async function fetchAllReads(
.map(item => item.id)
if (articleCoordinates.length > 0) {
console.log('📖 [Reads] Fetching article events for', articleCoordinates.length, 'articles')
// Parse coordinates and fetch events
const articlesToFetch: Array<{ pubkey: string; identifier: string }> = []
@@ -187,7 +174,6 @@ export async function fetchAllReads(
const validArticles = filterValidItems(articles)
const sortedReads = sortByReadingActivity(validArticles)
console.log('✅ [Reads] Processed', sortedReads.length, 'total reads')
return sortedReads
} catch (error) {

View File

@@ -34,7 +34,6 @@ export async function rebroadcastEvents(
// If we're in flight mode (only local relays connected) and user wants to broadcast to all relays, skip
if (broadcastToAll && !hasRemoteConnection) {
console.log('✈️ Flight mode: skipping rebroadcast to remote relays')
return
}
@@ -50,7 +49,6 @@ export async function rebroadcastEvents(
}
if (targetRelays.length === 0) {
console.log('📡 No target relays for rebroadcast')
return
}
@@ -58,7 +56,6 @@ export async function rebroadcastEvents(
const rebroadcastPromises = events.map(async (event) => {
try {
await relayPool.publish(targetRelays, event)
console.log('📡 Rebroadcast event', event.id?.slice(0, 8), 'to', targetRelays.length, 'relay(s)')
} catch (error) {
console.warn('⚠️ Failed to rebroadcast event', event.id?.slice(0, 8), error)
}
@@ -68,11 +65,5 @@ export async function rebroadcastEvents(
Promise.all(rebroadcastPromises).catch((err) => {
console.warn('⚠️ Some rebroadcasts failed:', err)
})
console.log(`📡 Rebroadcasting ${events.length} event(s) to ${targetRelays.length} relay(s)`, {
broadcastToAll,
useLocalCache,
targetRelays
})
}

View File

@@ -40,11 +40,7 @@ export function updateAndGetRelayStatuses(relayPool: RelayPool): RelayStatus[] {
const connectedCount = statuses.filter(s => s.isInPool).length
const disconnectedCount = statuses.filter(s => !s.isInPool).length
if (connectedCount === 0 || disconnectedCount > 0) {
console.log(`🔌 Relay status: ${connectedCount} connected, ${disconnectedCount} disconnected`)
const connected = statuses.filter(s => s.isInPool).map(s => s.url.replace(/^wss?:\/\//, ''))
const disconnected = statuses.filter(s => !s.isInPool).map(s => s.url.replace(/^wss?:\/\//, ''))
if (connected.length > 0) console.log('✅ Connected:', connected.join(', '))
if (disconnected.length > 0) console.log('❌ Disconnected:', disconnected.join(', '))
// Debug: relay status changed, but we're not logging it
}
// Add recently seen relays that are no longer connected

View File

@@ -71,7 +71,6 @@ export async function loadSettings(
pubkey: string,
relays: string[]
): Promise<UserSettings | null> {
console.log('⚙️ Loading settings from nostr...', { pubkey: pubkey.slice(0, 8) + '...', relays })
// First, check if we already have settings in the local event store
try {
@@ -80,7 +79,6 @@ export async function loadSettings(
)
if (localEvent) {
const content = getAppDataContent<UserSettings>(localEvent)
console.log('✅ Settings loaded from local store (cached):', content)
// Still fetch from relays in the background to get any updates
relayPool
@@ -94,8 +92,8 @@ export async function loadSettings(
return content || null
}
} catch (err) {
console.log('📭 No cached settings found, fetching from relays...')
} catch (_err) {
// Ignore local store errors
}
// If not in local store, fetch from relays
@@ -127,10 +125,8 @@ export async function loadSettings(
)
if (event) {
const content = getAppDataContent<UserSettings>(event)
console.log('✅ Settings loaded from relays:', content)
resolve(content || null)
} else {
console.log('📭 No settings event found - using defaults')
resolve(null)
}
} catch (err) {
@@ -161,7 +157,6 @@ export async function saveSettings(
factory: EventFactory,
settings: UserSettings
): Promise<void> {
console.log('💾 Saving settings to nostr:', settings)
// Create NIP-78 application data event manually
// Note: AppDataBlueprint is not available in the npm package
@@ -177,7 +172,6 @@ export async function saveSettings(
// Use unified write service
await publishEvent(relayPool, eventStore, signed)
console.log('✅ Settings published successfully')
}
export function watchSettings(

View File

@@ -78,7 +78,6 @@ export async function createWebBookmark(
// Publish to relays in the background (don't block UI)
relayPool.publish(relays, signedEvent)
.then(() => {
console.log('✅ Web bookmark published to', relays.length, 'relays:', signedEvent)
})
.catch((err) => {
console.warn('⚠️ Some relays failed to publish bookmark:', err)

View File

@@ -19,7 +19,6 @@ export async function publishEvent(
// Store the event in the local EventStore FIRST for immediate UI display
eventStore.add(event)
console.log(`${logPrefix} 💾 Stored event in EventStore:`, event.id.slice(0, 8), `(kind ${event.kind})`)
// Check current connection status - are we online or in flight mode?
const connectedRelays = Array.from(relayPool.relays.values())
@@ -35,14 +34,7 @@ export async function publishEvent(
const isLocalOnly = areAllRelaysLocal(expectedSuccessRelays)
console.log(`${logPrefix} 📍 Event relay status:`, {
targetRelays: RELAYS.length,
expectedSuccessRelays: expectedSuccessRelays.length,
isLocalOnly,
hasRemoteConnection,
eventId: event.id.slice(0, 8),
connectedRelays: connectedRelays.length
})
// Publishing event
// If we're in local-only mode, mark this event for later sync
if (isLocalOnly) {
@@ -50,10 +42,8 @@ export async function publishEvent(
}
// Publish to all configured relays in the background (non-blocking)
console.log(`${logPrefix} 📤 Publishing to relays:`, RELAYS)
relayPool.publish(RELAYS, event)
.then(() => {
console.log(`${logPrefix} ✅ Event published to`, RELAYS.length, 'relay(s):', event.id.slice(0, 8))
})
.catch((error) => {
console.warn(`${logPrefix} ⚠️ Failed to publish event to relays (event still saved locally):`, error)

View File

@@ -138,7 +138,6 @@ class WritingsController {
// Skip if already loaded for this pubkey (unless forced)
if (!force && this.isLoadedFor(pubkey)) {
console.log('[writings] ✅ Already loaded for', pubkey.slice(0, 8))
this.emitWritings(this.currentPosts)
return
}
@@ -148,7 +147,6 @@ class WritingsController {
const currentGeneration = this.generation
this.setLoading(true)
console.log('[writings] 🔍 Loading writings for', pubkey.slice(0, 8))
try {
const seenIds = new Set<string>()
@@ -162,7 +160,6 @@ class WritingsController {
}
if (lastSyncedAt) {
filter.since = lastSyncedAt
console.log('[writings] 📅 Incremental sync since', new Date(lastSyncedAt * 1000).toISOString())
}
const events = await queryEvents(
@@ -201,7 +198,6 @@ class WritingsController {
// Check if still active after async operation
if (currentGeneration !== this.generation) {
console.log('[writings] ⚠️ Load cancelled (generation mismatch)')
return
}
@@ -231,7 +227,6 @@ class WritingsController {
this.setLastSyncedAt(pubkey, newestTimestamp)
}
console.log('[writings] ✅ Loaded', sorted.length, 'writings')
} catch (error) {
console.error('[writings] ❌ Failed to load writings:', error)
this.currentPosts = []

View File

@@ -22,7 +22,6 @@ export async function fetchBorisZappers(
relayPool: RelayPool
): Promise<ZapSender[]> {
try {
console.log('⚡ Fetching zap receipts for Boris...', BORIS_PUBKEY)
// Use all configured relays plus specific zap-heavy relays
const zapRelays = [
@@ -63,23 +62,18 @@ export async function fetchBorisZappers(
merge(local$, remote$).pipe(toArray())
)
console.log(`📊 Fetched ${zapReceipts.length} raw zap receipts`)
// Dedupe by event ID and validate
const uniqueReceipts = new Map<string, NostrEvent>()
let invalidCount = 0
zapReceipts.forEach(receipt => {
if (!uniqueReceipts.has(receipt.id)) {
if (isValidZap(receipt)) {
uniqueReceipts.set(receipt.id, receipt)
} else {
invalidCount++
}
}
})
console.log(`${uniqueReceipts.size} valid zap receipts (${invalidCount} invalid)`)
// Aggregate by sender using applesauce helpers
const senderTotals = new Map<string, { totalSats: number; zapCount: number }>()
@@ -102,7 +96,6 @@ export async function fetchBorisZappers(
})
}
console.log(`👥 Found ${senderTotals.size} unique senders`)
// Filter >= 2100 sats, mark whales >= 69420 sats, sort by total desc
const zappers: ZapSender[] = Array.from(senderTotals.entries())
@@ -115,7 +108,6 @@ export async function fetchBorisZappers(
}))
.sort((a, b) => b.totalSats - a.totalSats)
console.log(`✅ Found ${zappers.length} supporters (${zappers.filter(z => z.isWhale).length} whales)`)
return zappers
} catch (error) {

View File

@@ -32,7 +32,7 @@
.individual-bookmarks h4 { margin: 0 0 1rem 0; font-size: 1rem; color: var(--color-text); }
.bookmarks-grid { display: flex; flex-direction: column; gap: 1rem; width: 100%; max-width: 100%; }
.bookmarks-grid.bookmarks-compact { gap: 0.5rem; }
.bookmarks-grid.bookmarks-compact { gap: 0.25rem; }
.bookmarks-grid.bookmarks-large { gap: 1.5rem; }
@media (max-width: 768px) {
.bookmarks-grid { gap: 0.75rem; }
@@ -44,9 +44,9 @@
.individual-bookmark:hover { border-color: var(--color-border); background: var(--color-bg-elevated); }
/* Compact view */
.individual-bookmark.compact { padding: 0.5rem 0.5rem; background: transparent; border: none !important; border-radius: 0; box-shadow: none; width: 100%; max-width: 100%; overflow: hidden; }
.individual-bookmark.compact { padding: 0.25rem 0.5rem; background: transparent; border: none !important; border-radius: 0; box-shadow: none; width: 100%; max-width: 100%; overflow: hidden; }
.individual-bookmark.compact:hover { background: var(--color-bg-elevated); transform: none; box-shadow: none; border: none !important; }
.compact-row { display: flex; align-items: center; gap: 0.5rem; height: 28px; width: 100%; min-width: 0; overflow: hidden; }
.compact-row { display: flex; align-items: center; gap: 0.5rem; height: 24px; width: 100%; min-width: 0; overflow: hidden; }
.compact-thumbnail { width: 24px; height: 24px; flex-shrink: 0; border-radius: 4px; overflow: hidden; background: var(--color-bg-elevated); display: flex; align-items: center; justify-content: center; }
.compact-thumbnail img { width: 100%; height: 100%; object-fit: cover; }
.compact-row.clickable { cursor: pointer; }

View File

@@ -109,6 +109,16 @@
background: var(--color-bg-elevated) !important;
}
.bookmarks-list .individual-bookmark.compact {
border: none !important;
background: transparent !important;
}
.bookmarks-list .individual-bookmark.compact:hover {
border-color: transparent !important;
background: var(--color-bg-elevated) !important;
}
.bookmark-item {
padding: 1rem;
background: var(--color-bg);

View File

@@ -22,7 +22,6 @@ cleanupOutdatedCaches()
sw.skipWaiting()
clientsClaim()
console.log('[SW] Boris service worker loaded')
// Runtime cache: Cross-origin images
// This preserves the existing image caching behavior

View File

@@ -16,18 +16,15 @@ const loadingFonts = new Map<string, Promise<void>>()
export async function loadFont(fontKey: string): Promise<void> {
if (fontKey === 'system') {
console.log('📝 Using system font')
return Promise.resolve()
}
if (loadedFonts.has(fontKey)) {
console.log('✅ Font already loaded:', fontKey)
return Promise.resolve()
}
// If font is currently loading, return the existing promise
if (loadingFonts.has(fontKey)) {
console.log('⏳ Font already loading:', fontKey)
return loadingFonts.get(fontKey)!
}
@@ -37,7 +34,6 @@ export async function loadFont(fontKey: string): Promise<void> {
return Promise.resolve()
}
console.log('🔤 Loading font:', fontFamily)
// Create a promise for this font loading
const loadPromise = new Promise<void>((resolve) => {
@@ -48,7 +44,6 @@ export async function loadFont(fontKey: string): Promise<void> {
// Wait for the stylesheet to load
link.onload = () => {
console.log('📄 Stylesheet loaded for:', fontFamily)
// Use Font Loading API to wait for the actual font to be ready
if ('fonts' in document) {
@@ -56,7 +51,6 @@ export async function loadFont(fontKey: string): Promise<void> {
document.fonts.load(`400 16px "${fontFamily}"`),
document.fonts.load(`700 16px "${fontFamily}"`)
]).then(() => {
console.log('✅ Font ready:', fontFamily)
loadedFonts.add(fontKey)
loadingFonts.delete(fontKey)
resolve()
@@ -69,7 +63,6 @@ export async function loadFont(fontKey: string): Promise<void> {
} else {
// Fallback: just wait a bit for older browsers
setTimeout(() => {
console.log('✅ Font assumed ready (no Font Loading API):', fontFamily)
loadedFonts.add(fontKey)
loadingFonts.delete(fontKey)
resolve()

View File

@@ -10,14 +10,9 @@ export function applyHighlightsToHTML(
highlightStyle: 'marker' | 'underline' = 'marker'
): string {
if (!html || highlights.length === 0) {
console.log('⚠️ applyHighlightsToHTML: No HTML or highlights', {
htmlLength: html?.length,
highlightsCount: highlights.length
})
return html
}
console.log('🔨 applyHighlightsToHTML: Processing', highlights.length, 'highlights')
const tempDiv = document.createElement('div')
tempDiv.innerHTML = html
@@ -31,9 +26,6 @@ export function applyHighlightsToHTML(
mark.parentNode?.replaceChild(textNode, mark)
})
console.log('🧹 Removed', existingMarks.length, 'existing highlight marks')
let appliedCount = 0
for (const highlight of highlights) {
const searchText = highlight.content.trim()
@@ -42,7 +34,6 @@ export function applyHighlightsToHTML(
continue
}
console.log('🔍 Searching for highlight:', searchText.substring(0, 50) + '...')
// Collect all text nodes
const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT, null)
@@ -50,21 +41,16 @@ export function applyHighlightsToHTML(
let node: Node | null
while ((node = walker.nextNode())) textNodes.push(node as Text)
console.log('📄 Found', textNodes.length, 'text nodes to search')
// Try exact match first, then normalized match
const found = tryMarkInTextNodes(textNodes, searchText, highlight, false, highlightStyle) ||
tryMarkInTextNodes(textNodes, searchText, highlight, true, highlightStyle)
if (found) {
appliedCount++
console.log('✅ Highlight applied successfully')
} else {
if (!found) {
console.warn('❌ Could not find match for highlight:', searchText.substring(0, 50))
}
}
console.log('🎉 Applied', appliedCount, '/', highlights.length, 'highlights')
return tempDiv.innerHTML
}

View File

@@ -62,7 +62,11 @@ export function filterByReadingProgress(
case 'reading':
return progress > 0.10 && progress <= 0.94 && !isMarked
case 'completed':
return progress >= 0.95 || isMarked
// Completed is 95%+ progress only (no emoji fallback)
return progress >= 0.95
case 'emoji':
// Emoji-marked items regardless of progress
return isMarked
case 'highlighted':
return hasHighlights
case 'all':

View File

@@ -43,8 +43,7 @@ export function applyTheme(
root.classList.add(`light-${lightColorTheme}`)
// Listen for system theme changes
mediaQueryListener = (e: MediaQueryListEvent) => {
console.log('🎨 System theme changed to:', e.matches ? 'dark' : 'light')
mediaQueryListener = () => {
// The CSS media query handles the color changes automatically
}
@@ -59,5 +58,4 @@ export function applyTheme(
}
}
console.log('🎨 Applied theme:', theme, 'with colors:', { dark: darkColorTheme, light: lightColorTheme })
}

View File

@@ -11,26 +11,21 @@ export function normalizeUrl(url: string): string {
export function filterHighlightsByUrl(highlights: Highlight[], selectedUrl: string | undefined): Highlight[] {
if (!selectedUrl || highlights.length === 0) {
console.log('🔍 filterHighlightsByUrl: No URL or highlights', { selectedUrl, count: highlights.length })
return []
}
console.log('🔍 filterHighlightsByUrl:', { selectedUrl, totalHighlights: highlights.length })
// For Nostr articles, we already fetched highlights specifically for this article
// So we don't need to filter them - they're all relevant
if (selectedUrl.startsWith('nostr:')) {
console.log('📌 Nostr article - returning all', highlights.length, 'highlights')
return highlights
}
// For web URLs, filter by URL matching
const normalizedSelected = normalizeUrl(selectedUrl)
console.log('🔗 Normalized selected URL:', normalizedSelected)
const filtered = highlights.filter(h => {
if (!h.urlReference) {
console.log('⚠️ Highlight has no urlReference:', h.id, 'eventReference:', h.eventReference)
return false
}
const normalizedRef = normalizeUrl(h.urlReference)
@@ -39,14 +34,13 @@ export function filterHighlightsByUrl(highlights: Highlight[], selectedUrl: stri
normalizedRef.includes(normalizedSelected)
if (matches) {
console.log('✅ URL match:', normalizedRef)
// URLs match
} else {
console.log('❌ URL mismatch:', normalizedRef, 'vs', normalizedSelected)
// URLs do not match
}
return matches
})
console.log('📊 Filtered to', filtered.length, 'highlights')
return filtered
}