feat: add offline highlight creation with local relay tracking

- Add relay tracking to Highlight type (publishedRelays, isLocalOnly fields)
- Create utility functions to identify local relays (localhost/127.0.0.1)
- Update highlight creation service to track which relays received the event
- Detect when highlights are only on local relays and mark accordingly
- Add visual indicator in UI for local-only highlights with amber badge
- Enable immediate display of highlights created offline
- Ensure highlights work even when only local relay is available
This commit is contained in:
Gigi
2025-10-09 12:40:04 +01:00
parent aa8332831f
commit 6636d540aa
6 changed files with 86 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faQuoteLeft, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'
import { faQuoteLeft, faExternalLinkAlt, faHouseSignal } from '@fortawesome/free-solid-svg-icons'
import { Highlight } from '../types/highlights'
import { formatDistanceToNow } from 'date-fns'
import { useEventModel } from 'applesauce-react/hooks'
@@ -91,6 +91,16 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({ highlight, onSelec
{formatDistanceToNow(new Date(highlight.created_at * 1000), { addSuffix: true })}
</span>
{highlight.isLocalOnly && (
<>
<span className="highlight-meta-separator"></span>
<span className="highlight-local-indicator" title="This highlight is only stored on your local relay">
<FontAwesomeIcon icon={faHouseSignal} />
<span className="highlight-local-text">Local</span>
</span>
</>
)}
{sourceLink && (
<a
href={sourceLink}

View File

@@ -3,7 +3,7 @@ import { RelayPool } from 'applesauce-relay'
import { NostrEvent } from 'nostr-tools'
import { Highlight } from '../types/highlights'
import { ReadableContent } from '../services/readerService'
import { createHighlight, eventToHighlight } from '../services/highlightCreationService'
import { createHighlight } from '../services/highlightCreationService'
import { HighlightButtonRef } from '../components/HighlightButton'
import { UserSettings } from '../services/settingsService'
@@ -54,7 +54,7 @@ export const useHighlightCreation = ({
? currentArticle.content
: readerContent?.markdown || readerContent?.html
const signedEvent = await createHighlight(
const newHighlight = await createHighlight(
text,
source,
activeAccount,
@@ -67,7 +67,6 @@ export const useHighlightCreation = ({
console.log('✅ Highlight created successfully!')
highlightButtonRef.current?.clearSelection()
const newHighlight = eventToHighlight(signedEvent)
onHighlightCreated(newHighlight)
} catch (error) {
console.error('Failed to create highlight:', error)

View File

@@ -1586,6 +1586,25 @@ body {
color: #888;
}
.highlight-local-indicator {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: #f59e0b;
font-weight: 500;
font-size: 0.8rem;
padding: 0.15rem 0.5rem;
background: rgba(245, 158, 11, 0.1);
border-radius: 4px;
border: 1px solid rgba(245, 158, 11, 0.3);
}
.highlight-local-text {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.highlight-source {
display: flex;
align-items: center;

View File

@@ -7,6 +7,7 @@ import { Helpers } from 'applesauce-core'
import { RELAYS } from '../config/relays'
import { Highlight } from '../types/highlights'
import { UserSettings } from './settingsService'
import { areAllRelaysLocal } from '../utils/helpers'
// Boris pubkey for zap splits
const BORIS_PUBKEY = '6e468422dfb74a5738702a8823b9b28168fc6cfb119d613e49ca0ec5a0bbd0c3'
@@ -26,7 +27,7 @@ const { HighlightBlueprint } = Blueprints
/**
* Creates and publishes a highlight event (NIP-84)
* Supports both nostr-native articles and external URLs
* Returns the signed event for immediate UI updates
* Returns a Highlight object with relay tracking info for immediate UI updates
*/
export async function createHighlight(
selectedText: string,
@@ -36,7 +37,7 @@ export async function createHighlight(
contentForContext?: string,
comment?: string,
settings?: UserSettings
): Promise<NostrEvent> {
): Promise<Highlight> {
if (!selectedText || !source) {
throw new Error('Missing required data to create highlight')
}
@@ -104,13 +105,34 @@ export async function createHighlight(
// Sign the event
const signedEvent = await factory.sign(highlightEvent)
// Get list of currently connected relays from the pool
const connectedRelays = Array.from(relayPool.relays.values()).map(relay => relay.url)
// Determine which relays we're publishing to (intersection of RELAYS and connected relays)
const publishingRelays = RELAYS.filter(url => connectedRelays.includes(url))
// If no relays are connected, fallback to just local relay if available
const targetRelays = publishingRelays.length > 0 ? publishingRelays : RELAYS.filter(r => r.includes('localhost') || r.includes('127.0.0.1'))
// Publish to relays (including local relay)
await relayPool.publish(RELAYS, signedEvent)
await relayPool.publish(targetRelays, signedEvent)
console.log('✅ Highlight published to', RELAYS.length, 'relays (including local):', signedEvent)
// Check if we're only publishing to local relays
const isLocalOnly = areAllRelaysLocal(targetRelays)
// Return the signed event for immediate UI updates
return signedEvent
console.log('✅ Highlight published to', targetRelays.length, 'relays:', {
relays: targetRelays,
isLocalOnly,
event: signedEvent
})
// Convert to Highlight with relay tracking info
const highlight = eventToHighlight(signedEvent)
highlight.publishedRelays = targetRelays
highlight.isLocalOnly = isLocalOnly
// Return the highlight for immediate UI updates
return highlight
}
/**

View File

@@ -15,5 +15,8 @@ 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
publishedRelays?: string[] // URLs of relays that acknowledged this event
isLocalOnly?: boolean // true if only published to local relays
}

View File

@@ -40,3 +40,26 @@ export const classifyUrl = (url: string | undefined): UrlClassification => {
return { type: 'article', buttonText: 'READ NOW' }
}
/**
* Checks if a relay URL is a local relay (localhost or 127.0.0.1)
*/
export const isLocalRelay = (relayUrl: string): boolean => {
return relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1')
}
/**
* Checks if all relays in the list are local relays
*/
export const areAllRelaysLocal = (relayUrls: string[]): boolean => {
if (!relayUrls || relayUrls.length === 0) return false
return relayUrls.every(isLocalRelay)
}
/**
* Checks if at least one relay is a remote (non-local) relay
*/
export const hasRemoteRelay = (relayUrls: string[]): boolean => {
if (!relayUrls || relayUrls.length === 0) return false
return relayUrls.some(url => !isLocalRelay(url))
}