Compare commits

...

34 Commits

Author SHA1 Message Date
Gigi
fa052483b2 chore: bump version to 0.10.26 2025-10-31 00:27:14 +01:00
Gigi
0ae9e6321e Merge pull request #29 from dergigi/flight-mode-shenanigans
Fix flight mode detection and persist highlight metadata
2025-10-31 00:26:08 +01:00
Gigi
5623f2e595 chore: remove debug console.log statements
- Remove debug console.log from AddBookmarkModal.tsx (modal fetch and description extraction)
- Remove console.debug from relayManager.ts (relay closing errors)
- Keep console.warn and console.error for legitimate error handling
2025-10-31 00:24:42 +01:00
Gigi
2c94c1e3f0 fix: remove unused variables to resolve lint errors
- Remove unused relayNames variable from HighlightItem.tsx
- Remove unused failedRelays variable from highlightCreationService.ts
- All linting and type checks now pass
2025-10-31 00:18:58 +01:00
Gigi
19dc2f70f2 feat: persist highlight metadata and offline events to localStorage
- Add localStorage persistence for highlightMetadataCache Map
- Add localStorage persistence for offlineCreatedEvents Set
- Load both caches from localStorage on module initialization
- Save to localStorage whenever caches are updated
- Update metadata cache during sync operations (isSyncing changes)
- Ensures airplane icon displays correctly after page reloads
- Gracefully handles localStorage errors and corrupted data
2025-10-31 00:17:13 +01:00
Gigi
5013ccc552 perf: remove excessive debug logging for better performance
- Remove debug logs from highlight creation, publishing, and UI rendering
- Keep only essential error logging
- Improves performance by reducing console spam
- Flight mode detection still works via fallback mechanisms
2025-10-31 00:12:04 +01:00
Gigi
29eed3395f fix: prioritize isLocalOnly check to show airplane icon
- Check isLocalOnly first before checking publishedRelays length
- Show airplane icon if isLocalOnly is true, even if publishedRelays is empty
- This ensures flight mode highlights show airplane icon via offline sync fallback
- Add debug logs to track cache storage and retrieval
- Fixes issue where airplane icon doesn't show when creating highlights offline
2025-10-31 00:09:09 +01:00
Gigi
d6da27c634 fix: resolve all linting errors and type issues
- Remove unused setShowOfflineIndicator calls
- Remove unused areAllRelaysLocal import
- Fix duplicate key 'failedRelays' in console.log
- Replace 'any' types with proper HighlightEvent interface
- Add eslint-disable comments for intentionally excluded useEffect dependencies
- All linting errors resolved, type checks pass
2025-10-31 00:04:24 +01:00
Gigi
5551b52bce fix: replace require() with ES module imports
- Add isEventOfflineCreated and isLocalRelay to imports
- Remove require() calls that don't work in ES modules
- Fixes ReferenceError: require is not defined
2025-10-31 00:02:29 +01:00
Gigi
af7eb48893 fix: add fallback logic for detecting flight mode highlights
- Add isEventOfflineCreated function to check offline sync service
- Use offline sync service as fallback if isLocalOnly is undefined
- Also check if publishedRelays only contains local relays
- This provides multiple fallback mechanisms to detect flight mode highlights
- Should finally fix the airplane icon not showing
2025-10-31 00:01:09 +01:00
Gigi
51ce79f13a fix: use metadata cache to preserve highlight properties across EventStore
- Create highlightMetadataCache to store isLocalOnly and publishedRelays
- Properties stored as __highlightProps are lost during EventStore serialization
- Cache persists across storage/retrieval cycles
- eventToHighlight now checks cache first before __highlightProps
- This should finally fix the airplane icon not showing for flight mode highlights
2025-10-30 23:56:53 +01:00
Gigi
bcfc04c35c debug: add logging to investigate tooltip showing all relays
- Add debug log to see what highlight data is available when rendering
- Check if publishedRelays or seenOnRelays are being used
- This will help identify why tooltip shows all relays instead of just published ones
2025-10-30 23:54:07 +01:00
Gigi
d6911b2acb fix: publish only to connected relays to avoid long timeouts
- Only publish to currently connected relays instead of all configured relays
- This prevents waiting for disconnected/offline relays to timeout
- Improves performance significantly in flight mode
- Handle case when no relays are connected
2025-10-30 23:53:44 +01:00
Gigi
2a869f11e0 fix: prevent duplicate highlights and remove excessive logging
- Add deduplication when adding highlights via onHighlightCreated
- Remove excessive UI logging that was causing performance issues
- Fixes duplicate highlight warning and improves render performance
2025-10-30 21:29:35 +01:00
Gigi
deab9974fa fix: remove excessive logging from eventToHighlight
- Remove console.log that was spamming console with EVENT-TO-HIGHLIGHT logs
- This function is called frequently during renders, causing performance issues
- Keep the function logic but remove the logging
2025-10-30 21:05:45 +01:00
Gigi
49872337f3 fix: manually set highlight properties after eventToHighlight
- Set publishedRelays, isLocalOnly, and isSyncing directly on highlight object
- This bypasses the __highlightProps mechanism which wasn't working
- Add logging to verify properties are set correctly
- This should finally fix the airplane icon not showing in flight mode
2025-10-30 21:01:18 +01:00
Gigi
389b4de0eb debug: add logging to eventToHighlight conversion
- Add [EVENT-TO-HIGHLIGHT] logs to see if __highlightProps are being preserved
- Add [HIGHLIGHT-CREATION] logs before calling eventToHighlight
- This will help identify why isLocalOnly and publishedRelays are undefined in final highlight
2025-10-30 20:59:43 +01:00
Gigi
959ccac857 fix: store event in EventStore after updating properties
- Move eventStore.add() call to AFTER updating __highlightProps with final values
- This ensures highlights loaded from EventStore have correct isLocalOnly and publishedRelays
- Reduce UI logging spam by only logging when values are meaningful
- This should fix the airplane icon not showing and reduce excessive re-renders
2025-10-30 20:56:50 +01:00
Gigi
78c58693a5 debug: enhance logging in HighlightItem to debug icon issue
- Log entire highlight object to see what properties are actually present
- Log publishedRelayCount and actualPublishedRelaysInUI for clarity
- Add conditionResult to show what icon should be displayed
- This will help identify why airplane icon isn't showing despite isLocalOnly being true
2025-10-30 20:54:15 +01:00
Gigi
ab81fe5030 debug: add logging to useHighlightCreation hook
- Add [HIGHLIGHT-CREATION] logs to track highlight creation flow
- Log when createHighlight function is called
- Log highlight properties after creation to verify isLocalOnly is set
- This will help debug why [HIGHLIGHT-PUBLISH] logs are missing
2025-10-30 20:48:53 +01:00
Gigi
6bae30070e fix: preserve isLocalOnly and publishedRelays in eventToHighlight
- Attach custom properties to event as __highlightProps before storing
- Update eventToHighlight to preserve these properties when converting
- This ensures highlights loaded from EventStore maintain flight mode status
- Fixes issue where isLocalOnly was undefined in UI component
- The airplane icon should now show correctly for flight mode highlights
2025-10-30 20:44:58 +01:00
Gigi
1f6a904717 debug: add comprehensive logging for flight mode detection
- Add detailed logs with [HIGHLIGHT-PUBLISH] prefix for publication process
- Add detailed logs with [HIGHLIGHT-UI] prefix for UI rendering
- Log relay responses, success/failure analysis, and flight mode reasoning
- Log icon decision making process in UI component
- This will help debug why airplane icon isn't showing in flight mode
2025-10-30 20:42:02 +01:00
Gigi
9379475d1c feat: implement proper relay response tracking for flight mode
- Use pool.publish() which returns individual relay responses
- Track which relays actually accepted the event (response.ok === true)
- Set isLocalOnly = true only when only local relays accepted the event
- This provides accurate flight mode detection based on actual publishing success
- Debug logging shows all relay responses for troubleshooting
2025-10-30 20:41:24 +01:00
Gigi
77a5f4bd2a refactor: implement proper flight mode detection for highlights
- Always publish to all relays but track which ones are actually connected
- isLocalOnly = true when only local relays are connected (flight mode)
- Store event in EventStore first for immediate UI display
- Track actually connected relays instead of guessing based on connection status
- This should fix the airplane icon not showing in flight mode
2025-10-30 20:40:20 +01:00
Gigi
4fa01231cd fix: determine isLocalOnly before publishing, not after
- Move connection status check before publishEvent call
- Set isLocalOnly based on actual connection state at creation time
- This ensures airplane icon shows correctly in flight mode
- Previous logic was checking after publishing, which was too late
2025-10-30 20:35:51 +01:00
Gigi
1cd85507a7 debug: add logging to highlight creation isLocalOnly logic
- Add console.log to debug why isLocalOnly is not being set correctly
- Fix logic: isLocalOnly should be true when only local relays are connected
- Previous logic was checking expectedSuccessRelays instead of actual connections
- This will help identify why airplane icon doesn't show in flight mode
2025-10-30 20:34:23 +01:00
Gigi
b6f151c711 refactor: use isLocalOnly instead of isOfflineCreated
- isLocalOnly is more accurate - covers both offline and online-but-local-only scenarios
- Update tooltip to 'Local relays only - will sync when remote relays available'
- Better semantic meaning: highlights only published to local relays
- Covers cases where user is online but only connected to local relays
2025-10-30 20:32:43 +01:00
Gigi
e3d924f3fc refactor: remove redundant isLocalOnly flag
- Remove isLocalOnly field from Highlight type and creation logic
- Use only isOfflineCreated flag for flight mode highlights
- Simplify UI logic to check only isOfflineCreated
- Both flags were set to the same value, making them redundant
- Cleaner, more maintainable code with single source of truth
2025-10-30 20:31:09 +01:00
Gigi
5914df23d3 fix: show airplane icon for flight mode highlights
- Simplify logic to check highlight.isOfflineCreated || highlight.isLocalOnly
- Show airplane icon with 'Created offline - will sync when online' tooltip
- Remove complex showOfflineIndicator state management
- Fixes issue where flight mode highlights showed highlighter icon instead of plane icon
2025-10-30 20:30:04 +01:00
Gigi
2083c2b8c6 fix: remove eventStore and setter functions from useEffect dependencies
- Remove eventStore from useArticleLoader and useExternalUrlLoader dependencies
- Remove setter functions from dependencies as they shouldn't change
- Only keep naddr/url and previewData/cachedUrlHighlights as dependencies
- This prevents content loaders from re-running when going offline
- Fixes the core issue where loading skeleton appears immediately on offline detection
2025-10-30 20:23:23 +01:00
Gigi
35a8411d9b fix: check EventStore before setting loading state
- Move EventStore check before setReaderLoading(true) call
- Only show loading skeleton if content not found in store and no preview data
- This prevents loading skeleton from appearing when cached content is available
- Fixes the core issue where offline mode shows loading skeleton with cached content
2025-10-30 20:21:59 +01:00
Gigi
15b3b5b990 fix: remove relayPool dependency from content loaders
- Remove relayPool from useEffect dependencies in useArticleLoader and useExternalUrlLoader
- This prevents content reloading when relay status changes (going offline/online)
- Content loaders now only re-run when the actual content identifier changes
- Fixes issue where loading skeleton appears when going offline with cached content
2025-10-30 20:20:07 +01:00
Gigi
ad56acb712 fix: prevent unnecessary relay queries when article content is cached
- Return early from useArticleLoader when content is found in EventStore
- This prevents loading skeleton from showing when going offline with cached content
- Improves offline experience by using locally cached article content
2025-10-30 20:18:06 +01:00
Gigi
2f5fe87fc8 docs: update CHANGELOG for v0.10.25 2025-10-25 01:59:51 +02:00
13 changed files with 409 additions and 117 deletions

View File

@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.10.25] - 2025-01-27
### Added
- Reading progress bar for all bookmark types across all view modes
- Consistent progress tracking for articles, videos, and other content types
- Visual progress indicator in compact, medium, and large card views
- Title display for regular bookmarks/links
- Better content identification for web bookmarks
- Improved visual hierarchy in bookmark cards
### Changed
- Redesigned medium-sized bookmark cards with left-side thumbnails
- More compact and visually appealing card layout
- Better use of space with thumbnail positioning
- Made bookmark cards significantly more compact
- Reduced vertical spacing and improved density
- Better information display in limited space
- Moved bookmark type icon to bottom right footer
- Cleaner card layout with consistent icon positioning
- Better visual organization of card elements
- Enhanced card layout with author positioning in bottom-left corner
- Improved visual hierarchy and content organization
- Better balance between content and metadata
### Fixed
- Corrected TypeScript error in content type icon logic
- Resolved linting issues in CardView component
- Eliminated 0 artefacts in compact view conditional rendering
- Ensured consistent reading progress bar thickness in large cards
- Fixed reading progress bar display logic for compact view
- Ensured empty reading progress bar is always visible for articles
- Resolved timestamp and icon display alignment issues
- Removed unused variables to resolve linting errors
### Removed
- Type icon from medium-sized bookmark cards
- Cleaner visual design with reduced clutter
- Focus on content rather than type indicators
- Text expansion mechanic from medium-sized cards
- Simplified interaction model
- More consistent card behavior across view modes
- URL display from medium-sized bookmark cards
- Cleaner card appearance
- Better focus on title and content
## [0.10.24] - 2025-01-27
### Added

View File

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

View File

@@ -88,13 +88,6 @@ const AddBookmarkModal: React.FC<AddBookmarkModalProps> = ({ onClose, onSave })
fetchOpenGraph(normalizedUrl).catch(() => null) // Don't fail if OpenGraph fetch fails
])
console.log('🔍 Modal fetch debug:', {
url: normalizedUrl,
hasContent: !!content,
hasOgData: !!ogData,
ogDataKeys: ogData ? Object.keys(ogData) : null
})
lastFetchedUrlRef.current = normalizedUrl
let extractedAnything = false
@@ -121,13 +114,6 @@ const AddBookmarkModal: React.FC<AddBookmarkModalProps> = ({ onClose, onSave })
if (!description && ogData) {
const extractedDesc = ogData['og:description'] || ogData['twitter:description'] || ogData.description
console.log('🔍 Description extraction debug:', {
currentDescription: description,
hasOgData: !!ogData,
extractedDesc: extractedDesc,
willSetDescription: !!extractedDesc
})
if (extractedDesc) {
setDescription(extractedDesc)
extractedAnything = true

View File

@@ -229,7 +229,14 @@ const Bookmarks: React.FC<BookmarksProps> = ({
currentArticle,
selectedUrl,
readerContent,
onHighlightCreated: (highlight) => setHighlights(prev => [highlight, ...prev]),
onHighlightCreated: (highlight) => setHighlights(prev => {
// Deduplicate by checking if highlight with this ID already exists
const exists = prev.some(h => h.id === highlight.id)
if (exists) {
return prev // Don't add duplicate
}
return [highlight, ...prev]
}),
settings
})

View File

@@ -7,8 +7,8 @@ import { useEventModel } from 'applesauce-react/hooks'
import { Models, IEventStore } from 'applesauce-core'
import { RelayPool } from 'applesauce-relay'
import { Hooks } from 'applesauce-react'
import { onSyncStateChange, isEventSyncing } from '../services/offlineSyncService'
import { areAllRelaysLocal } from '../utils/helpers'
import { onSyncStateChange, isEventSyncing, isEventOfflineCreated } from '../services/offlineSyncService'
import { areAllRelaysLocal, isLocalRelay } from '../utils/helpers'
import { getActiveRelayUrls } from '../services/relayManager'
import { nip19 } from 'nostr-tools'
import { formatDateCompact } from '../utils/bookmarkUtils'
@@ -114,7 +114,6 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
const itemRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const [isSyncing, setIsSyncing] = useState(() => isEventSyncing(highlight.id))
const [showOfflineIndicator, setShowOfflineIndicator] = useState(() => highlight.isOfflineCreated && !isSyncing)
const [isRebroadcasting, setIsRebroadcasting] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
@@ -133,12 +132,6 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
return `${highlight.pubkey.slice(0, 8)}...` // fallback to short pubkey
}
// Update offline indicator when highlight prop changes
useEffect(() => {
if (highlight.isOfflineCreated && !isSyncing) {
setShowOfflineIndicator(true)
}
}, [highlight.isOfflineCreated, isSyncing])
// Listen to sync state changes
useEffect(() => {
@@ -147,8 +140,6 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
setIsSyncing(syncingState)
// When sync completes successfully, update highlight to show all relays
if (!syncingState) {
setShowOfflineIndicator(false)
// Update the highlight with all relays after successful sync
if (onHighlightUpdate && highlight.isLocalOnly && relayPool) {
const updatedHighlight = {
@@ -292,9 +283,6 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
onHighlightUpdate(updatedHighlight)
}
// Update local state
setShowOfflineIndicator(false)
} catch (error) {
console.error('❌ Failed to rebroadcast:', error)
} finally {
@@ -313,8 +301,37 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
}
}
// Always show relay list, use plane icon for local-only
const isLocalOrOffline = highlight.isLocalOnly || showOfflineIndicator
// Check if this highlight was only published to local relays
let isLocalOnly = highlight.isLocalOnly
const publishedRelays = highlight.publishedRelays || []
// Fallback 1: Check if this highlight was marked for offline sync (flight mode)
if (isLocalOnly === undefined) {
if (isEventOfflineCreated(highlight.id)) {
isLocalOnly = true
}
}
// Fallback 2: If publishedRelays only contains local relays, it's local-only
if (isLocalOnly === undefined && publishedRelays.length > 0) {
const hasOnlyLocalRelays = publishedRelays.every(url => isLocalRelay(url))
const hasRemoteRelays = publishedRelays.some(url => !isLocalRelay(url))
if (hasOnlyLocalRelays && !hasRemoteRelays) {
isLocalOnly = true
}
}
// If isLocalOnly is true (from any fallback), show airplane icon
if (isLocalOnly === true) {
return {
icon: faPlane,
tooltip: publishedRelays.length > 0
? 'Local relays only - will sync when remote relays available'
: 'Created in flight mode - will sync when remote relays available',
spin: false
}
}
// Show highlighter icon with relay info if available
if (highlight.publishedRelays && highlight.publishedRelays.length > 0) {
@@ -322,7 +339,7 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
url.replace(/^wss?:\/\//, '').replace(/\/$/, '')
)
return {
icon: isLocalOrOffline ? faPlane : faHighlighter,
icon: faHighlighter,
tooltip: relayNames.join('\n'),
spin: false
}

View File

@@ -85,47 +85,18 @@ export function useArticleLoader({
// when we know the article coordinate
setHighlightsLoading(false) // Don't show loading yet
// If we have preview data from navigation, show it immediately (no skeleton!)
if (previewData) {
setCurrentTitle(previewData.title)
setReaderContent({
title: previewData.title,
markdown: '', // Will be loaded from store or relay
image: previewData.image,
summary: previewData.summary,
published: previewData.published,
url: `nostr:${naddr}`
})
setReaderLoading(false) // Turn off loading immediately - we have the preview!
} else {
setReaderLoading(true)
setReaderContent(undefined)
}
try {
// Decode naddr to filter
const decoded = nip19.decode(naddr)
if (decoded.type !== 'naddr') {
throw new Error('Invalid naddr format')
}
const pointer = decoded.data as AddressPointer
const filter = {
kinds: [pointer.kind],
authors: [pointer.pubkey],
'#d': [pointer.identifier]
}
let firstEmitted = false
let latestEvent: NostrEvent | null = null
// Check eventStore first for instant load (from bookmark cards, explore, etc.)
if (eventStore) {
try {
// Check eventStore first for instant load (from bookmark cards, explore, etc.)
let foundInStore = false
if (eventStore) {
try {
// Decode naddr to get the coordinate
const decoded = nip19.decode(naddr)
if (decoded.type === 'naddr') {
const pointer = decoded.data as AddressPointer
const coordinate = `${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`
const storedEvent = eventStore.getEvent?.(coordinate)
if (storedEvent) {
latestEvent = storedEvent as NostrEvent
firstEmitted = true
foundInStore = true
const title = Helpers.getArticleTitle(storedEvent) || 'Untitled Article'
setCurrentTitle(title)
const image = Helpers.getArticleImage(storedEvent)
@@ -145,11 +116,50 @@ export function useArticleLoader({
setCurrentArticleEventId(storedEvent.id)
setCurrentArticle?.(storedEvent)
setReaderLoading(false)
// If we found the content in EventStore, we can return early
// This prevents unnecessary relay queries when offline
return
}
} catch (err) {
// Ignore store errors, fall through to relay query
}
} catch (err) {
// Ignore store errors, fall through to relay query
}
}
// If we have preview data from navigation, show it immediately (no skeleton!)
if (previewData) {
setCurrentTitle(previewData.title)
setReaderContent({
title: previewData.title,
markdown: '', // Will be loaded from store or relay
image: previewData.image,
summary: previewData.summary,
published: previewData.published,
url: `nostr:${naddr}`
})
setReaderLoading(false) // Turn off loading immediately - we have the preview!
} else if (!foundInStore) {
// Only show loading if we didn't find content in store and no preview data
setReaderLoading(true)
setReaderContent(undefined)
}
try {
// Decode naddr to filter
const decoded = nip19.decode(naddr)
if (decoded.type !== 'naddr') {
throw new Error('Invalid naddr format')
}
const pointer = decoded.data as AddressPointer
const filter = {
kinds: [pointer.kind],
authors: [pointer.pubkey],
'#d': [pointer.identifier]
}
let firstEmitted = false
let latestEvent: NostrEvent | null = null
// Stream local-first via queryEvents; rely on EOSE (no timeouts)
const events = await queryEvents(relayPool, filter, {
@@ -305,19 +315,11 @@ export function useArticleLoader({
return () => {
mountedRef.current = false
}
// Dependencies intentionally excluded to prevent re-renders when relay/eventStore state changes
// This fixes the loading skeleton appearing when going offline (flight mode)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
naddr,
relayPool,
eventStore,
previewData,
setSelectedUrl,
setReaderContent,
setReaderLoading,
setIsCollapsed,
setHighlights,
setHighlightsLoading,
setCurrentArticleCoordinate,
setCurrentArticleEventId,
setCurrentArticle
previewData
])
}

View File

@@ -165,19 +165,12 @@ export function useExternalUrlLoader({
return () => {
mountedRef.current = false
}
// Dependencies intentionally excluded to prevent re-renders when relay/eventStore state changes
// This fixes the loading skeleton appearing when going offline (flight mode)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
url,
relayPool,
eventStore,
cachedUrlHighlights,
setReaderContent,
setReaderLoading,
setIsCollapsed,
setSelectedUrl,
setHighlights,
setCurrentArticleCoordinate,
setCurrentArticleEventId,
setHighlightsLoading
cachedUrlHighlights
])
// Keep UI highlights synced with cached store updates without reloading content

View File

@@ -44,6 +44,7 @@ export const useHighlightCreation = ({
}, [])
const handleCreateHighlight = useCallback(async (text: string) => {
if (!activeAccount || !relayPool || !eventStore) {
console.error('Missing requirements for highlight creation')
return
@@ -60,7 +61,6 @@ export const useHighlightCreation = ({
? currentArticle.content
: readerContent?.markdown || readerContent?.html
const newHighlight = await createHighlight(
text,
source,
@@ -73,7 +73,6 @@ export const useHighlightCreation = ({
)
// Highlight created successfully
// Clear the browser's text selection immediately to allow DOM update
const selection = window.getSelection()
if (selection) {

View File

@@ -7,13 +7,22 @@ import { Helpers, IEventStore } from 'applesauce-core'
import { RELAYS } from '../config/relays'
import { Highlight } from '../types/highlights'
import { UserSettings } from './settingsService'
import { isLocalRelay, areAllRelaysLocal } from '../utils/helpers'
import { publishEvent } from './writeService'
import { isLocalRelay } from '../utils/helpers'
import { setHighlightMetadata } from './highlightEventProcessor'
// Boris pubkey for zap splits
// npub19802see0gnk3vjlus0dnmfdagusqrtmsxpl5yfmkwn9uvnfnqylqduhr0x
export const BORIS_PUBKEY = '29dea8672f44ed164bfc83db3da5bd472001af70307f42277674cbc64d33013e'
// Extended event type with highlight metadata
interface HighlightEvent extends NostrEvent {
__highlightProps?: {
publishedRelays?: string[]
isLocalOnly?: boolean
isSyncing?: boolean
}
}
const {
getHighlightText,
getHighlightContext,
@@ -118,25 +127,111 @@ export async function createHighlight(
// Sign the event
const signedEvent = await factory.sign(highlightEvent)
// Use unified write service to store and publish
await publishEvent(relayPool, eventStore, signedEvent)
// Initialize custom properties on the event (will be updated after publishing)
;(signedEvent as HighlightEvent).__highlightProps = {
publishedRelays: [],
isLocalOnly: false,
isSyncing: false
}
// Check current connection status for UI feedback
// Get only connected relays to avoid long timeouts
const connectedRelays = Array.from(relayPool.relays.values())
.filter(relay => relay.connected)
.map(relay => relay.url)
let publishResponses: { ok: boolean; message?: string; from: string }[] = []
let isLocalOnly = false
const hasRemoteConnection = connectedRelays.some(url => !isLocalRelay(url))
const expectedSuccessRelays = hasRemoteConnection
? RELAYS
: RELAYS.filter(isLocalRelay)
const isLocalOnly = areAllRelaysLocal(expectedSuccessRelays)
// Convert to Highlight with relay tracking info and return IMMEDIATELY
try {
// Publish only to connected relays to avoid long timeouts
if (connectedRelays.length === 0) {
isLocalOnly = true
} else {
publishResponses = await relayPool.publish(connectedRelays, signedEvent)
}
// Determine which relays successfully accepted the event
const successfulRelays = publishResponses
.filter(response => response.ok)
.map(response => response.from)
const successfulLocalRelays = successfulRelays.filter(url => isLocalRelay(url))
const successfulRemoteRelays = successfulRelays.filter(url => !isLocalRelay(url))
// isLocalOnly is true if only local relays accepted the event
isLocalOnly = successfulLocalRelays.length > 0 && successfulRemoteRelays.length === 0
// Handle case when no relays were connected
const successfulRelaysList = publishResponses.length > 0
? publishResponses
.filter(response => response.ok)
.map(response => response.from)
: []
// Store metadata in cache (persists across EventStore serialization)
setHighlightMetadata(signedEvent.id, {
publishedRelays: successfulRelaysList,
isLocalOnly,
isSyncing: false
})
// Also update the event with the actual properties (for backwards compatibility)
;(signedEvent as HighlightEvent).__highlightProps = {
publishedRelays: successfulRelaysList,
isLocalOnly,
isSyncing: false
}
// Store the event in EventStore AFTER updating with final properties
eventStore.add(signedEvent)
// Mark for offline sync if we're in local-only mode
if (isLocalOnly) {
const { markEventAsOfflineCreated } = await import('./offlineSyncService')
markEventAsOfflineCreated(signedEvent.id)
}
} catch (error) {
console.error('❌ [HIGHLIGHT-PUBLISH] Failed to publish highlight to relays:', error)
// If publishing fails completely, assume local-only mode
isLocalOnly = true
// Store metadata in cache (persists across EventStore serialization)
setHighlightMetadata(signedEvent.id, {
publishedRelays: [],
isLocalOnly: true,
isSyncing: false
})
// Also update the event with the error state (for backwards compatibility)
;(signedEvent as HighlightEvent).__highlightProps = {
publishedRelays: [],
isLocalOnly: true,
isSyncing: false
}
// Store the event in EventStore AFTER updating with final properties
eventStore.add(signedEvent)
const { markEventAsOfflineCreated } = await import('./offlineSyncService')
markEventAsOfflineCreated(signedEvent.id)
}
// Convert to Highlight with relay tracking info
const highlight = eventToHighlight(signedEvent)
highlight.publishedRelays = expectedSuccessRelays
// Manually set the properties since __highlightProps might not be working
const finalPublishedRelays = publishResponses.length > 0
? publishResponses
.filter(response => response.ok)
.map(response => response.from)
: []
highlight.publishedRelays = finalPublishedRelays
highlight.isLocalOnly = isLocalOnly
highlight.isOfflineCreated = isLocalOnly
highlight.isSyncing = false
return highlight
}

View File

@@ -2,6 +2,15 @@ import { NostrEvent } from 'nostr-tools'
import { Helpers } from 'applesauce-core'
import { Highlight } from '../types/highlights'
// Extended event type with highlight metadata
interface HighlightEvent extends NostrEvent {
__highlightProps?: {
publishedRelays?: string[]
isLocalOnly?: boolean
isSyncing?: boolean
}
}
const {
getHighlightText,
getHighlightContext,
@@ -12,6 +21,66 @@ const {
getHighlightAttributions
} = Helpers
const METADATA_CACHE_KEY = 'highlightMetadataCache'
type HighlightMetadata = {
publishedRelays?: string[]
isLocalOnly?: boolean
isSyncing?: boolean
}
/**
* Load highlight metadata from localStorage
*/
function loadHighlightMetadataFromStorage(): Map<string, HighlightMetadata> {
try {
const raw = localStorage.getItem(METADATA_CACHE_KEY)
if (!raw) return new Map()
const parsed = JSON.parse(raw) as Record<string, HighlightMetadata>
return new Map(Object.entries(parsed))
} catch {
// Silently fail on parse errors or if storage is unavailable
return new Map()
}
}
/**
* Save highlight metadata to localStorage
*/
function saveHighlightMetadataToStorage(cache: Map<string, HighlightMetadata>): void {
try {
const record = Object.fromEntries(cache.entries())
localStorage.setItem(METADATA_CACHE_KEY, JSON.stringify(record))
} catch {
// Silently fail if storage is full or unavailable
}
}
/**
* Cache for highlight metadata that persists across EventStore serialization
* Key: event ID, Value: { publishedRelays, isLocalOnly, isSyncing }
*/
const highlightMetadataCache = loadHighlightMetadataFromStorage()
/**
* Store highlight metadata for an event ID
*/
export function setHighlightMetadata(
eventId: string,
metadata: HighlightMetadata
): void {
highlightMetadataCache.set(eventId, metadata)
saveHighlightMetadataToStorage(highlightMetadataCache)
}
/**
* Get highlight metadata for an event ID
*/
export function getHighlightMetadata(eventId: string): HighlightMetadata | undefined {
return highlightMetadataCache.get(eventId)
}
/**
* Convert a NostrEvent to a Highlight object
*/
@@ -28,6 +97,12 @@ export function eventToHighlight(event: NostrEvent): Highlight {
const eventReference = sourceEventPointer?.id ||
(sourceAddressPointer ? `${sourceAddressPointer.kind}:${sourceAddressPointer.pubkey}:${sourceAddressPointer.identifier}` : undefined)
// Check cache first (persists across EventStore serialization)
const cachedMetadata = getHighlightMetadata(event.id)
// Fall back to __highlightProps if cache doesn't have it (for backwards compatibility)
const customProps = cachedMetadata || (event as HighlightEvent).__highlightProps || {}
return {
id: event.id,
pubkey: event.pubkey,
@@ -38,7 +113,11 @@ export function eventToHighlight(event: NostrEvent): Highlight {
urlReference: sourceUrl,
author,
context,
comment
comment,
// Preserve custom properties if they exist
publishedRelays: customProps.publishedRelays,
isLocalOnly: customProps.isLocalOnly,
isSyncing: customProps.isSyncing
}
}

View File

@@ -3,11 +3,42 @@ import { NostrEvent } from 'nostr-tools'
import { IEventStore } from 'applesauce-core'
import { RELAYS } from '../config/relays'
import { isLocalRelay } from '../utils/helpers'
import { setHighlightMetadata, getHighlightMetadata } from './highlightEventProcessor'
const OFFLINE_EVENTS_KEY = 'offlineCreatedEvents'
let isSyncing = false
/**
* Load offline events from localStorage
*/
function loadOfflineEventsFromStorage(): Set<string> {
try {
const raw = localStorage.getItem(OFFLINE_EVENTS_KEY)
if (!raw) return new Set()
const parsed = JSON.parse(raw) as string[]
return new Set(parsed)
} catch {
// Silently fail on parse errors or if storage is unavailable
return new Set()
}
}
/**
* Save offline events to localStorage
*/
function saveOfflineEventsToStorage(events: Set<string>): void {
try {
const array = Array.from(events)
localStorage.setItem(OFFLINE_EVENTS_KEY, JSON.stringify(array))
} catch {
// Silently fail if storage is full or unavailable
}
}
// Track events created during offline period
const offlineCreatedEvents = new Set<string>()
const offlineCreatedEvents = loadOfflineEventsFromStorage()
// Track events currently being synced
const syncingEvents = new Set<string>()
@@ -20,6 +51,14 @@ const syncStateListeners: Array<(eventId: string, isSyncing: boolean) => void> =
*/
export function markEventAsOfflineCreated(eventId: string): void {
offlineCreatedEvents.add(eventId)
saveOfflineEventsToStorage(offlineCreatedEvents)
}
/**
* Check if an event was created during offline period (flight mode)
*/
export function isEventOfflineCreated(eventId: string): boolean {
return offlineCreatedEvents.has(eventId)
}
/**
@@ -87,6 +126,7 @@ export async function syncLocalEventsToRemote(
if (eventsToSync.length === 0) {
isSyncing = false
offlineCreatedEvents.clear()
saveOfflineEventsToStorage(offlineCreatedEvents)
return
}
@@ -95,10 +135,17 @@ export async function syncLocalEventsToRemote(
new Map(eventsToSync.map(e => [e.id, e])).values()
)
// Mark all events as syncing
// Mark all events as syncing and update metadata
uniqueEvents.forEach(event => {
syncingEvents.add(event.id)
notifySyncStateChange(event.id, true)
// Update metadata cache to reflect syncing state
const existingMetadata = getHighlightMetadata(event.id)
setHighlightMetadata(event.id, {
...existingMetadata,
isSyncing: true
})
})
// Publish to remote relays
@@ -118,13 +165,32 @@ export async function syncLocalEventsToRemote(
syncingEvents.delete(eventId)
offlineCreatedEvents.delete(eventId)
notifySyncStateChange(eventId, false)
// Update metadata cache: sync complete, no longer local-only
const existingMetadata = getHighlightMetadata(eventId)
setHighlightMetadata(eventId, {
...existingMetadata,
isSyncing: false,
isLocalOnly: false
})
})
// Save updated offline events set to localStorage
saveOfflineEventsToStorage(offlineCreatedEvents)
// Clear syncing state for failed events
uniqueEvents.forEach(event => {
if (!successfulIds.includes(event.id)) {
syncingEvents.delete(event.id)
notifySyncStateChange(event.id, false)
// Update metadata cache: sync failed, still local-only
const existingMetadata = getHighlightMetadata(event.id)
setHighlightMetadata(event.id, {
...existingMetadata,
isSyncing: false
// Keep isLocalOnly as true (sync failed)
})
}
})
} catch (error) {

View File

@@ -88,7 +88,7 @@ export function applyRelaySetToPool(
} catch (error) {
// Suppress errors when closing relays that haven't fully connected yet
// This can happen when switching relay sets before connections establish
console.debug('[relay-manager] Ignoring error when closing relay:', url, error)
// Silently ignore
}
relayPool.relays.delete(url)
}

View File

@@ -15,11 +15,10 @@ export interface Highlight {
comment?: string // optional comment about the highlight
// Level classification (computed based on user's context)
level?: HighlightLevel
// Relay tracking for offline/local-only highlights
// Relay tracking for local-only highlights
publishedRelays?: string[] // URLs of relays where this was published (for user-created highlights)
seenOnRelays?: string[] // URLs of relays where this event was fetched from
isLocalOnly?: boolean // true if only published to local relays
isOfflineCreated?: boolean // true if created while in flight mode (offline)
isSyncing?: boolean // true if currently being synced to remote relays
}