mirror of
https://github.com/dergigi/boris.git
synced 2025-12-24 01:54:19 +01:00
feat(highlights): add click-to-rebroadcast functionality to relay indicator
Make relay indicator icons clickable to trigger manual rebroadcast to all connected relays: - Click plane icon (local/offline) to rebroadcast to remote relays - Click server icon to rebroadcast to all relays - Show spinner while rebroadcasting - Update icon from plane to server on successful rebroadcast - Keep plane icon on failure - Pass relayPool and eventStore through component chain - Add local state management for highlight updates in HighlightsPanel - Enhance CSS with scale animation on hover/active
This commit is contained in:
@@ -192,6 +192,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
}}
|
||||
onRefresh={handleRefreshAll}
|
||||
relayPool={relayPool}
|
||||
eventStore={eventStore}
|
||||
readerLoading={readerLoading}
|
||||
readerContent={readerContent}
|
||||
selectedUrl={selectedUrl}
|
||||
|
||||
@@ -4,8 +4,11 @@ import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer } from '@f
|
||||
import { Highlight } from '../types/highlights'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { useEventModel } from 'applesauce-react/hooks'
|
||||
import { Models } from 'applesauce-core'
|
||||
import { Models, IEventStore } from 'applesauce-core'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { onSyncStateChange, isEventSyncing } from '../services/offlineSyncService'
|
||||
import { RELAYS } from '../config/relays'
|
||||
import { areAllRelaysLocal } from '../utils/helpers'
|
||||
|
||||
interface HighlightWithLevel extends Highlight {
|
||||
level?: 'mine' | 'friends' | 'nostrverse'
|
||||
@@ -16,12 +19,24 @@ interface HighlightItemProps {
|
||||
onSelectUrl?: (url: string) => void
|
||||
isSelected?: boolean
|
||||
onHighlightClick?: (highlightId: string) => void
|
||||
relayPool?: RelayPool | null
|
||||
eventStore?: IEventStore | null
|
||||
onHighlightUpdate?: (highlight: Highlight) => void
|
||||
}
|
||||
|
||||
export const HighlightItem: React.FC<HighlightItemProps> = ({ highlight, onSelectUrl, isSelected, onHighlightClick }) => {
|
||||
export const HighlightItem: React.FC<HighlightItemProps> = ({
|
||||
highlight,
|
||||
onSelectUrl,
|
||||
isSelected,
|
||||
onHighlightClick,
|
||||
relayPool,
|
||||
eventStore,
|
||||
onHighlightUpdate
|
||||
}) => {
|
||||
const itemRef = useRef<HTMLDivElement>(null)
|
||||
const [isSyncing, setIsSyncing] = useState(() => isEventSyncing(highlight.id))
|
||||
const [showOfflineIndicator, setShowOfflineIndicator] = useState(() => highlight.isOfflineCreated && !isSyncing)
|
||||
const [isRebroadcasting, setIsRebroadcasting] = useState(false)
|
||||
|
||||
// Resolve the profile of the user who made the highlight
|
||||
const profile = useEventModel(Models.ProfileModel, [highlight.pubkey])
|
||||
@@ -83,16 +98,82 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({ highlight, onSelec
|
||||
|
||||
const sourceLink = getSourceLink()
|
||||
|
||||
// Handle rebroadcast to all relays
|
||||
const handleRebroadcast = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation() // Prevent triggering highlight selection
|
||||
|
||||
if (!relayPool || !eventStore || isRebroadcasting) return
|
||||
|
||||
setIsRebroadcasting(true)
|
||||
|
||||
try {
|
||||
// Get the event from the event store
|
||||
const event = eventStore.getEvent(highlight.id)
|
||||
if (!event) {
|
||||
console.error('Event not found in store:', highlight.id)
|
||||
return
|
||||
}
|
||||
|
||||
// Get all connected relays
|
||||
const connectedRelays = Array.from(relayPool.relays.values())
|
||||
.filter(relay => relay.connected)
|
||||
.map(relay => relay.url)
|
||||
|
||||
// Publish to all connected relays
|
||||
const targetRelays = RELAYS.filter(url => connectedRelays.includes(url))
|
||||
|
||||
if (targetRelays.length === 0) {
|
||||
console.warn('No connected relays to rebroadcast to')
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
const updatedHighlight = {
|
||||
...highlight,
|
||||
publishedRelays: targetRelays,
|
||||
isLocalOnly,
|
||||
isOfflineCreated: false
|
||||
}
|
||||
|
||||
// Notify parent of the update
|
||||
if (onHighlightUpdate) {
|
||||
onHighlightUpdate(updatedHighlight)
|
||||
}
|
||||
|
||||
// Update local state
|
||||
setShowOfflineIndicator(false)
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to rebroadcast:', error)
|
||||
} finally {
|
||||
setIsRebroadcasting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine relay indicator icon and tooltip
|
||||
const getRelayIndicatorInfo = () => {
|
||||
if (isRebroadcasting) {
|
||||
return {
|
||||
icon: faSpinner,
|
||||
tooltip: 'Rebroadcasting to all relays...',
|
||||
spin: true
|
||||
}
|
||||
}
|
||||
|
||||
const isLocalOrOffline = highlight.isLocalOnly || (showOfflineIndicator && !isSyncing)
|
||||
|
||||
if (isLocalOrOffline) {
|
||||
return {
|
||||
icon: faPlane,
|
||||
tooltip: highlight.isLocalOnly
|
||||
? 'Local only (not published to remote relays)'
|
||||
: 'Created in flight mode'
|
||||
tooltip: 'Click to rebroadcast to all relays',
|
||||
spin: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +186,8 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({ highlight, onSelec
|
||||
)
|
||||
return {
|
||||
icon: faServer,
|
||||
tooltip: `Published to ${relayNames.length} relay(s):\n${relayNames.join('\n')}`
|
||||
tooltip: `Published to ${relayNames.length} relay(s):\n${relayNames.join('\n')}\n\nClick to rebroadcast`,
|
||||
spin: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,8 +204,13 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({ highlight, onSelec
|
||||
<div className="highlight-quote-icon">
|
||||
<FontAwesomeIcon icon={faQuoteLeft} />
|
||||
{relayIndicator && (
|
||||
<div className="highlight-relay-indicator" title={relayIndicator.tooltip}>
|
||||
<FontAwesomeIcon icon={relayIndicator.icon} />
|
||||
<div
|
||||
className="highlight-relay-indicator"
|
||||
title={relayIndicator.tooltip}
|
||||
onClick={handleRebroadcast}
|
||||
style={{ cursor: relayPool && eventStore ? 'pointer' : 'default' }}
|
||||
>
|
||||
<FontAwesomeIcon icon={relayIndicator.icon} spin={relayIndicator.spin} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { HighlightItem } from './HighlightItem'
|
||||
import { useFilteredHighlights } from '../hooks/useFilteredHighlights'
|
||||
import HighlightsPanelCollapsed from './HighlightsPanel/HighlightsPanelCollapsed'
|
||||
import HighlightsPanelHeader from './HighlightsPanel/HighlightsPanelHeader'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { IEventStore } from 'applesauce-core'
|
||||
|
||||
export interface HighlightVisibility {
|
||||
nostrverse: boolean
|
||||
@@ -28,6 +30,8 @@ interface HighlightsPanelProps {
|
||||
highlightVisibility?: HighlightVisibility
|
||||
onHighlightVisibilityChange?: (visibility: HighlightVisibility) => void
|
||||
followedPubkeys?: Set<string>
|
||||
relayPool?: RelayPool | null
|
||||
eventStore?: IEventStore | null
|
||||
}
|
||||
|
||||
export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
||||
@@ -44,9 +48,12 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
||||
currentUserPubkey,
|
||||
highlightVisibility = { nostrverse: true, friends: true, mine: true },
|
||||
onHighlightVisibilityChange,
|
||||
followedPubkeys = new Set()
|
||||
followedPubkeys = new Set(),
|
||||
relayPool,
|
||||
eventStore
|
||||
}) => {
|
||||
const [showHighlights, setShowHighlights] = useState(true)
|
||||
const [localHighlights, setLocalHighlights] = useState(highlights)
|
||||
|
||||
const handleToggleHighlights = () => {
|
||||
const newValue = !showHighlights
|
||||
@@ -54,8 +61,19 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
||||
onToggleHighlights?.(newValue)
|
||||
}
|
||||
|
||||
// Keep track of highlight updates
|
||||
React.useEffect(() => {
|
||||
setLocalHighlights(highlights)
|
||||
}, [highlights])
|
||||
|
||||
const handleHighlightUpdate = (updatedHighlight: Highlight) => {
|
||||
setLocalHighlights(prev =>
|
||||
prev.map(h => h.id === updatedHighlight.id ? updatedHighlight : h)
|
||||
)
|
||||
}
|
||||
|
||||
const filteredHighlights = useFilteredHighlights({
|
||||
highlights,
|
||||
highlights: localHighlights,
|
||||
selectedUrl,
|
||||
highlightVisibility,
|
||||
currentUserPubkey,
|
||||
@@ -108,6 +126,9 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
||||
onSelectUrl={onSelectUrl}
|
||||
isSelected={highlight.id === selectedHighlightId}
|
||||
onHighlightClick={onHighlightClick}
|
||||
relayPool={relayPool}
|
||||
eventStore={eventStore}
|
||||
onHighlightUpdate={handleHighlightUpdate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -140,6 +140,8 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
||||
highlightVisibility={props.highlightVisibility}
|
||||
onHighlightVisibilityChange={props.onHighlightVisibilityChange}
|
||||
followedPubkeys={props.followedPubkeys}
|
||||
relayPool={props.relayPool}
|
||||
eventStore={props.eventStore}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1575,13 +1575,18 @@ body {
|
||||
font-size: 0.7rem;
|
||||
color: #888;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
cursor: help;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.highlight-relay-indicator:hover {
|
||||
opacity: 1;
|
||||
color: #aaa;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.highlight-relay-indicator:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Level-colored quote icon */
|
||||
|
||||
Reference in New Issue
Block a user