mirror of
https://github.com/dergigi/boris.git
synced 2025-12-18 07:04:19 +01:00
feat: add relay rebroadcast settings for caching and propagation
- Add two new settings: - Use local relay(s) as cache (default: enabled) - Rebroadcast events to all relays (default: disabled) - Create rebroadcastService to handle rebroadcasting events - Hook into article, bookmark, and highlight fetching services - Automatically rebroadcast fetched events based on settings: - Articles when opened - Bookmarks when fetched - Highlights when fetched - Add RelayRebroadcastSettings component with plane/globe icons - Benefits: - Local caching for offline access - Content propagation across nostr network - User control over bandwidth usage
This commit is contained in:
@@ -88,7 +88,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
accountManager,
|
||||
naddr,
|
||||
currentArticleCoordinate,
|
||||
currentArticleEventId
|
||||
currentArticleEventId,
|
||||
settings
|
||||
})
|
||||
|
||||
const {
|
||||
@@ -134,7 +135,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
setHighlightsLoading,
|
||||
setCurrentArticleCoordinate,
|
||||
setCurrentArticleEventId,
|
||||
setCurrentArticle
|
||||
setCurrentArticle,
|
||||
settings
|
||||
})
|
||||
|
||||
// Load external URL if /r/* route is used
|
||||
|
||||
@@ -9,6 +9,7 @@ import LayoutNavigationSettings from './Settings/LayoutNavigationSettings'
|
||||
import StartupPreferencesSettings from './Settings/StartupPreferencesSettings'
|
||||
import ZapSettings from './Settings/ZapSettings'
|
||||
import RelaySettings from './Settings/RelaySettings'
|
||||
import RelayRebroadcastSettings from './Settings/RelayRebroadcastSettings'
|
||||
import { useRelayStatus } from '../hooks/useRelayStatus'
|
||||
|
||||
const DEFAULT_SETTINGS: UserSettings = {
|
||||
@@ -30,6 +31,8 @@ const DEFAULT_SETTINGS: UserSettings = {
|
||||
zapSplitHighlighterWeight: 50,
|
||||
zapSplitBorisWeight: 2.1,
|
||||
zapSplitAuthorWeight: 50,
|
||||
useLocalRelayAsCache: true,
|
||||
rebroadcastToAllRelays: false,
|
||||
}
|
||||
|
||||
interface SettingsProps {
|
||||
@@ -159,6 +162,7 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose, relayPoo
|
||||
<LayoutNavigationSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
<StartupPreferencesSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
<ZapSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
<RelayRebroadcastSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
<RelaySettings relayStatuses={relayStatuses} onClose={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
71
src/components/Settings/RelayRebroadcastSettings.tsx
Normal file
71
src/components/Settings/RelayRebroadcastSettings.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faPlane, faGlobe, faInfoCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { UserSettings } from '../../services/settingsService'
|
||||
|
||||
interface RelayRebroadcastSettingsProps {
|
||||
settings: UserSettings
|
||||
onUpdate: (updates: Partial<UserSettings>) => void
|
||||
}
|
||||
|
||||
const RelayRebroadcastSettings: React.FC<RelayRebroadcastSettingsProps> = ({
|
||||
settings,
|
||||
onUpdate
|
||||
}) => {
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<h3>Relay Rebroadcast</h3>
|
||||
|
||||
<div className="settings-group">
|
||||
<label className="settings-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.useLocalRelayAsCache ?? true}
|
||||
onChange={(e) => onUpdate({ useLocalRelayAsCache: e.target.checked })}
|
||||
/>
|
||||
<div className="settings-label-content">
|
||||
<div className="settings-label-title">
|
||||
<FontAwesomeIcon icon={faPlane} style={{ marginRight: '0.5rem', color: '#f59e0b' }} />
|
||||
Use local relay(s) as cache
|
||||
</div>
|
||||
<div className="settings-label-description">
|
||||
Rebroadcast articles, bookmarks, and highlights to your local relays when fetched.
|
||||
Helps keep your data available offline.
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<label className="settings-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.rebroadcastToAllRelays ?? false}
|
||||
onChange={(e) => onUpdate({ rebroadcastToAllRelays: e.target.checked })}
|
||||
/>
|
||||
<div className="settings-label-content">
|
||||
<div className="settings-label-title">
|
||||
<FontAwesomeIcon icon={faGlobe} style={{ marginRight: '0.5rem', color: '#646cff' }} />
|
||||
Rebroadcast events to all relays
|
||||
</div>
|
||||
<div className="settings-label-description">
|
||||
Rebroadcast articles, bookmarks, and highlights to all your relays.
|
||||
Helps propagate content across the nostr network.
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="settings-info-box">
|
||||
<FontAwesomeIcon icon={faInfoCircle} style={{ marginRight: '0.5rem' }} />
|
||||
<div>
|
||||
<strong>Why rebroadcast?</strong> Rebroadcasting helps preserve content and makes it available
|
||||
on more relays. Local caching ensures you can access your bookmarks and highlights even when offline.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RelayRebroadcastSettings
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fetchHighlightsForArticle } from '../services/highlightService'
|
||||
import { ReadableContent } from '../services/readerService'
|
||||
import { Highlight } from '../types/highlights'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
import { UserSettings } from '../services/settingsService'
|
||||
|
||||
interface UseArticleLoaderProps {
|
||||
naddr: string | undefined
|
||||
@@ -18,6 +19,7 @@ interface UseArticleLoaderProps {
|
||||
setCurrentArticleCoordinate: (coord: string | undefined) => void
|
||||
setCurrentArticleEventId: (id: string | undefined) => void
|
||||
setCurrentArticle?: (article: NostrEvent) => void
|
||||
settings?: UserSettings
|
||||
}
|
||||
|
||||
export function useArticleLoader({
|
||||
@@ -31,7 +33,8 @@ export function useArticleLoader({
|
||||
setHighlightsLoading,
|
||||
setCurrentArticleCoordinate,
|
||||
setCurrentArticleEventId,
|
||||
setCurrentArticle
|
||||
setCurrentArticle,
|
||||
settings
|
||||
}: UseArticleLoaderProps) {
|
||||
useEffect(() => {
|
||||
if (!relayPool || !naddr) return
|
||||
@@ -44,7 +47,7 @@ export function useArticleLoader({
|
||||
// Keep highlights panel collapsed by default - only open on user interaction
|
||||
|
||||
try {
|
||||
const article = await fetchArticleByNaddr(relayPool, naddr)
|
||||
const article = await fetchArticleByNaddr(relayPool, naddr, false, settings)
|
||||
setReaderContent({
|
||||
title: article.title,
|
||||
markdown: article.markdown,
|
||||
@@ -86,7 +89,8 @@ export function useArticleLoader({
|
||||
const highlightsList = Array.from(highlightsMap.values())
|
||||
setHighlights(highlightsList.sort((a, b) => b.created_at - a.created_at))
|
||||
}
|
||||
}
|
||||
},
|
||||
settings
|
||||
)
|
||||
console.log(`📌 Found ${highlightsMap.size} highlights`)
|
||||
} catch (err) {
|
||||
@@ -106,5 +110,5 @@ export function useArticleLoader({
|
||||
}
|
||||
|
||||
loadArticle()
|
||||
}, [naddr, relayPool, setSelectedUrl, setReaderContent, setReaderLoading, setIsCollapsed, setHighlights, setHighlightsLoading, setCurrentArticleCoordinate, setCurrentArticleEventId, setCurrentArticle])
|
||||
}, [naddr, relayPool, setSelectedUrl, setReaderContent, setReaderLoading, setIsCollapsed, setHighlights, setHighlightsLoading, setCurrentArticleCoordinate, setCurrentArticleEventId, setCurrentArticle, settings])
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Highlight } from '../types/highlights'
|
||||
import { fetchBookmarks } from '../services/bookmarkService'
|
||||
import { fetchHighlights, fetchHighlightsForArticle } from '../services/highlightService'
|
||||
import { fetchContacts } from '../services/contactService'
|
||||
import { UserSettings } from '../services/settingsService'
|
||||
|
||||
interface UseBookmarksDataParams {
|
||||
relayPool: RelayPool | null
|
||||
@@ -15,6 +16,7 @@ interface UseBookmarksDataParams {
|
||||
naddr?: string
|
||||
currentArticleCoordinate?: string
|
||||
currentArticleEventId?: string
|
||||
settings?: UserSettings
|
||||
}
|
||||
|
||||
export const useBookmarksData = ({
|
||||
@@ -23,7 +25,8 @@ export const useBookmarksData = ({
|
||||
accountManager,
|
||||
naddr,
|
||||
currentArticleCoordinate,
|
||||
currentArticleEventId
|
||||
currentArticleEventId,
|
||||
settings
|
||||
}: UseBookmarksDataParams) => {
|
||||
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
||||
const [bookmarksLoading, setBookmarksLoading] = useState(true)
|
||||
@@ -43,11 +46,11 @@ export const useBookmarksData = ({
|
||||
setBookmarksLoading(true)
|
||||
try {
|
||||
const fullAccount = accountManager.getActive()
|
||||
await fetchBookmarks(relayPool, fullAccount || activeAccount, setBookmarks)
|
||||
await fetchBookmarks(relayPool, fullAccount || activeAccount, setBookmarks, settings)
|
||||
} finally {
|
||||
setBookmarksLoading(false)
|
||||
}
|
||||
}, [relayPool, activeAccount, accountManager])
|
||||
}, [relayPool, activeAccount, accountManager, settings])
|
||||
|
||||
const handleFetchHighlights = useCallback(async () => {
|
||||
if (!relayPool) return
|
||||
@@ -67,11 +70,12 @@ export const useBookmarksData = ({
|
||||
const highlightsList = Array.from(highlightsMap.values())
|
||||
setHighlights(highlightsList.sort((a, b) => b.created_at - a.created_at))
|
||||
}
|
||||
}
|
||||
},
|
||||
settings
|
||||
)
|
||||
console.log(`🔄 Refreshed ${highlightsMap.size} highlights for article`)
|
||||
} else if (activeAccount) {
|
||||
const fetchedHighlights = await fetchHighlights(relayPool, activeAccount.pubkey)
|
||||
const fetchedHighlights = await fetchHighlights(relayPool, activeAccount.pubkey, undefined, settings)
|
||||
setHighlights(fetchedHighlights)
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -79,7 +83,7 @@ export const useBookmarksData = ({
|
||||
} finally {
|
||||
setHighlightsLoading(false)
|
||||
}
|
||||
}, [relayPool, activeAccount, currentArticleCoordinate, currentArticleEventId])
|
||||
}, [relayPool, activeAccount, currentArticleCoordinate, currentArticleEventId, settings])
|
||||
|
||||
const handleRefreshAll = useCallback(async () => {
|
||||
if (!relayPool || !activeAccount || isRefreshing) return
|
||||
|
||||
@@ -5,6 +5,8 @@ import { AddressPointer } from 'nostr-tools/nip19'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
import { Helpers } from 'applesauce-core'
|
||||
import { RELAYS } from '../config/relays'
|
||||
import { UserSettings } from './settingsService'
|
||||
import { rebroadcastEvents } from './rebroadcastService'
|
||||
|
||||
const { getArticleTitle, getArticleImage, getArticlePublished, getArticleSummary } = Helpers
|
||||
|
||||
@@ -71,11 +73,13 @@ function saveToCache(naddr: string, content: ArticleContent): void {
|
||||
* @param relayPool - The relay pool to query
|
||||
* @param naddr - The article's naddr
|
||||
* @param bypassCache - If true, skip cache and fetch fresh from relays
|
||||
* @param settings - User settings for rebroadcast options
|
||||
*/
|
||||
export async function fetchArticleByNaddr(
|
||||
relayPool: RelayPool,
|
||||
naddr: string,
|
||||
bypassCache = false
|
||||
bypassCache = false,
|
||||
settings?: UserSettings
|
||||
): Promise<ArticleContent> {
|
||||
try {
|
||||
// Check cache first unless bypassed
|
||||
@@ -120,6 +124,9 @@ export async function fetchArticleByNaddr(
|
||||
events.sort((a, b) => b.created_at - a.created_at)
|
||||
const article = events[0]
|
||||
|
||||
// Rebroadcast article to local/all relays based on settings
|
||||
await rebroadcastEvents([article], relayPool, settings)
|
||||
|
||||
const title = getArticleTitle(article) || 'Untitled Article'
|
||||
const image = getArticleImage(article)
|
||||
const published = getArticlePublished(article)
|
||||
|
||||
@@ -14,13 +14,16 @@ import {
|
||||
} from './bookmarkHelpers'
|
||||
import { Bookmark } from '../types/bookmarks'
|
||||
import { collectBookmarksFromEvents } from './bookmarkProcessing.ts'
|
||||
import { UserSettings } from './settingsService'
|
||||
import { rebroadcastEvents } from './rebroadcastService'
|
||||
|
||||
|
||||
|
||||
export const fetchBookmarks = async (
|
||||
relayPool: RelayPool,
|
||||
activeAccount: unknown, // Full account object with extension capabilities
|
||||
setBookmarks: (bookmarks: Bookmark[]) => void
|
||||
setBookmarks: (bookmarks: Bookmark[]) => void,
|
||||
settings?: UserSettings
|
||||
) => {
|
||||
try {
|
||||
|
||||
@@ -38,6 +41,9 @@ export const fetchBookmarks = async (
|
||||
)
|
||||
console.log('📊 Raw events fetched:', rawEvents.length, 'events')
|
||||
|
||||
// Rebroadcast bookmark events to local/all relays based on settings
|
||||
await rebroadcastEvents(rawEvents, relayPool, settings)
|
||||
|
||||
// Check for events with potentially encrypted content
|
||||
const eventsWithContent = rawEvents.filter(evt => evt.content && evt.content.length > 0)
|
||||
if (eventsWithContent.length > 0) {
|
||||
|
||||
@@ -4,18 +4,23 @@ import { NostrEvent } from 'nostr-tools'
|
||||
import { Highlight } from '../types/highlights'
|
||||
import { RELAYS } from '../config/relays'
|
||||
import { eventToHighlight, dedupeHighlights, sortHighlights } from './highlightEventProcessor'
|
||||
import { UserSettings } from './settingsService'
|
||||
import { rebroadcastEvents } from './rebroadcastService'
|
||||
|
||||
/**
|
||||
* Fetches highlights for a specific article by its address coordinate and/or event ID
|
||||
* @param relayPool - The relay pool to query
|
||||
* @param articleCoordinate - The article's address in format "kind:pubkey:identifier" (e.g., "30023:abc...def:my-article")
|
||||
* @param eventId - Optional event ID to also query by 'e' tag
|
||||
* @param onHighlight - Optional callback to receive highlights as they arrive
|
||||
* @param settings - User settings for rebroadcast options
|
||||
*/
|
||||
export const fetchHighlightsForArticle = async (
|
||||
relayPool: RelayPool,
|
||||
articleCoordinate: string,
|
||||
eventId?: string,
|
||||
onHighlight?: (highlight: Highlight) => void
|
||||
onHighlight?: (highlight: Highlight) => void,
|
||||
settings?: UserSettings
|
||||
): Promise<Highlight[]> => {
|
||||
try {
|
||||
console.log('🔍 Fetching highlights (kind 9802) for article:', articleCoordinate)
|
||||
@@ -75,6 +80,9 @@ export const fetchHighlightsForArticle = async (
|
||||
const rawEvents = [...aTagEvents, ...eTagEvents]
|
||||
console.log('📊 Total raw highlight events fetched:', rawEvents.length)
|
||||
|
||||
// Rebroadcast highlight events to local/all relays based on settings
|
||||
await rebroadcastEvents(rawEvents, relayPool, settings)
|
||||
|
||||
if (rawEvents.length > 0) {
|
||||
console.log('📄 Sample highlight tags:', JSON.stringify(rawEvents[0].tags, null, 2))
|
||||
} else {
|
||||
@@ -99,10 +107,12 @@ export const fetchHighlightsForArticle = async (
|
||||
* Fetches highlights for a specific URL
|
||||
* @param relayPool - The relay pool to query
|
||||
* @param url - The external URL to find highlights for
|
||||
* @param settings - User settings for rebroadcast options
|
||||
*/
|
||||
export const fetchHighlightsForUrl = async (
|
||||
relayPool: RelayPool,
|
||||
url: string
|
||||
url: string,
|
||||
settings?: UserSettings
|
||||
): Promise<Highlight[]> => {
|
||||
try {
|
||||
console.log('🔍 Fetching highlights (kind 9802) for URL:', url)
|
||||
@@ -124,6 +134,9 @@ export const fetchHighlightsForUrl = async (
|
||||
|
||||
console.log('📊 Highlights for URL:', rawEvents.length)
|
||||
|
||||
// Rebroadcast highlight events to local/all relays based on settings
|
||||
await rebroadcastEvents(rawEvents, relayPool, settings)
|
||||
|
||||
const uniqueEvents = dedupeHighlights(rawEvents)
|
||||
const highlights: Highlight[] = uniqueEvents.map(eventToHighlight)
|
||||
return sortHighlights(highlights)
|
||||
@@ -138,11 +151,13 @@ export const fetchHighlightsForUrl = async (
|
||||
* @param relayPool - The relay pool to query
|
||||
* @param pubkey - The user's public key
|
||||
* @param onHighlight - Optional callback to receive highlights as they arrive
|
||||
* @param settings - User settings for rebroadcast options
|
||||
*/
|
||||
export const fetchHighlights = async (
|
||||
relayPool: RelayPool,
|
||||
pubkey: string,
|
||||
onHighlight?: (highlight: Highlight) => void
|
||||
onHighlight?: (highlight: Highlight) => void,
|
||||
settings?: UserSettings
|
||||
): Promise<Highlight[]> => {
|
||||
try {
|
||||
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
|
||||
@@ -172,6 +187,9 @@ export const fetchHighlights = async (
|
||||
|
||||
console.log('📊 Raw highlight events fetched:', rawEvents.length)
|
||||
|
||||
// Rebroadcast highlight events to local/all relays based on settings
|
||||
await rebroadcastEvents(rawEvents, relayPool, settings)
|
||||
|
||||
// Deduplicate and process events
|
||||
const uniqueEvents = dedupeHighlights(rawEvents)
|
||||
console.log('📊 Unique highlight events after deduplication:', uniqueEvents.length)
|
||||
|
||||
67
src/services/rebroadcastService.ts
Normal file
67
src/services/rebroadcastService.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
import { UserSettings } from './settingsService'
|
||||
import { RELAYS } from '../config/relays'
|
||||
import { isLocalRelay } from '../utils/helpers'
|
||||
|
||||
/**
|
||||
* Rebroadcasts events to relays based on user settings
|
||||
* @param events Events to rebroadcast
|
||||
* @param relayPool The relay pool to use for publishing
|
||||
* @param settings User settings to determine which relays to broadcast to
|
||||
*/
|
||||
export async function rebroadcastEvents(
|
||||
events: NostrEvent[],
|
||||
relayPool: RelayPool,
|
||||
settings?: UserSettings
|
||||
): Promise<void> {
|
||||
if (!events || events.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if any rebroadcast is enabled
|
||||
const useLocalCache = settings?.useLocalRelayAsCache ?? true
|
||||
const broadcastToAll = settings?.rebroadcastToAllRelays ?? false
|
||||
|
||||
if (!useLocalCache && !broadcastToAll) {
|
||||
return // No rebroadcast enabled
|
||||
}
|
||||
|
||||
// Determine target relays based on settings
|
||||
let targetRelays: string[] = []
|
||||
|
||||
if (broadcastToAll) {
|
||||
// Broadcast to all relays
|
||||
targetRelays = RELAYS
|
||||
} else if (useLocalCache) {
|
||||
// Only broadcast to local relays
|
||||
targetRelays = RELAYS.filter(isLocalRelay)
|
||||
}
|
||||
|
||||
if (targetRelays.length === 0) {
|
||||
console.log('📡 No target relays for rebroadcast')
|
||||
return
|
||||
}
|
||||
|
||||
// Rebroadcast each event
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
// Execute all rebroadcasts (don't block on completion)
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ export interface UserSettings {
|
||||
zapSplitHighlighterWeight?: number // default 50
|
||||
zapSplitBorisWeight?: number // default 2.1
|
||||
zapSplitAuthorWeight?: number // default 50
|
||||
// Relay rebroadcast settings
|
||||
useLocalRelayAsCache?: boolean // Rebroadcast events to local relays
|
||||
rebroadcastToAllRelays?: boolean // Rebroadcast events to all relays
|
||||
}
|
||||
|
||||
export async function loadSettings(
|
||||
|
||||
Reference in New Issue
Block a user