mirror of
https://github.com/dergigi/boris.git
synced 2026-01-10 18:34:28 +01:00
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:
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user