mirror of
https://github.com/dergigi/boris.git
synced 2025-12-23 17:44:19 +01:00
feat: implement optimistic updates for highlight creation
- Update createHighlight to return the signed NostrEvent - Add eventToHighlight helper to convert events to Highlight objects - Immediately add new highlights to UI without fetching from relays - Remove handleHighlightCreated refresh logic (no longer needed) - Improves UX with instant feedback when creating highlights
This commit is contained in:
2
dist/index.html
vendored
2
dist/index.html
vendored
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Boris - Nostr Bookmarks</title>
|
||||
<script type="module" crossorigin src="/assets/index-BkbQg5P5.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CsB0QtFa.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bqz-n1DY.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Bookmark } from '../types/bookmarks'
|
||||
import { Highlight } from '../types/highlights'
|
||||
import { BookmarkList } from './BookmarkList'
|
||||
import { fetchBookmarks } from '../services/bookmarkService'
|
||||
import { fetchHighlights, fetchHighlightsForArticle, fetchHighlightsForUrl } from '../services/highlightService'
|
||||
import { fetchHighlights, fetchHighlightsForArticle } from '../services/highlightService'
|
||||
import { fetchContacts } from '../services/contactService'
|
||||
import ContentPanel from './ContentPanel'
|
||||
import { HighlightsPanel } from './HighlightsPanel'
|
||||
@@ -20,7 +20,7 @@ import { useExternalUrlLoader } from '../hooks/useExternalUrlLoader'
|
||||
import { loadContent, BookmarkReference } from '../utils/contentLoader'
|
||||
import { HighlightVisibility } from './HighlightsPanel'
|
||||
import { HighlightButton, HighlightButtonRef } from './HighlightButton'
|
||||
import { createHighlight } from '../services/highlightCreationService'
|
||||
import { createHighlight, eventToHighlight } from '../services/highlightCreationService'
|
||||
import { useRef, useCallback } from 'react'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
export type ViewMode = 'compact' | 'cards' | 'large'
|
||||
@@ -223,30 +223,6 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleHighlightCreated = async () => {
|
||||
// Refresh highlights after creating a new one
|
||||
if (!relayPool) return
|
||||
|
||||
try {
|
||||
// Refresh based on what we're currently viewing
|
||||
if (currentArticleCoordinate) {
|
||||
// Viewing a nostr article - fetch by article coordinate
|
||||
const newHighlights = await fetchHighlightsForArticle(
|
||||
relayPool,
|
||||
currentArticleCoordinate,
|
||||
currentArticleEventId
|
||||
)
|
||||
setHighlights(newHighlights)
|
||||
} else if (selectedUrl && !selectedUrl.startsWith('nostr:')) {
|
||||
// Viewing an external URL - fetch by URL
|
||||
const newHighlights = await fetchHighlightsForUrl(relayPool, selectedUrl)
|
||||
setHighlights(newHighlights)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh highlights:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTextSelection = useCallback((text: string) => {
|
||||
highlightButtonRef.current?.updateSelection(text)
|
||||
}, [])
|
||||
@@ -276,7 +252,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
? currentArticle.content
|
||||
: readerContent?.markdown || readerContent?.html
|
||||
|
||||
await createHighlight(
|
||||
// Create and publish the highlight
|
||||
const signedEvent = await createHighlight(
|
||||
text,
|
||||
source,
|
||||
activeAccount,
|
||||
@@ -287,12 +264,13 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
console.log('✅ Highlight created successfully!')
|
||||
highlightButtonRef.current?.clearSelection()
|
||||
|
||||
// Trigger refresh of highlights
|
||||
handleHighlightCreated()
|
||||
// Immediately add the highlight to the UI (optimistic update)
|
||||
const newHighlight = eventToHighlight(signedEvent)
|
||||
setHighlights(prev => [newHighlight, ...prev])
|
||||
} catch (error) {
|
||||
console.error('Failed to create highlight:', error)
|
||||
}
|
||||
}, [activeAccount, relayPool, currentArticle, selectedUrl, readerContent, handleHighlightCreated])
|
||||
}, [activeAccount, relayPool, currentArticle, selectedUrl, readerContent])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -5,10 +5,21 @@ import { IAccount } from 'applesauce-accounts'
|
||||
import { AddressPointer } from 'nostr-tools/nip19'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
import { RELAYS } from '../config/relays'
|
||||
import { Highlight } from '../types/highlights'
|
||||
import {
|
||||
getHighlightText,
|
||||
getHighlightContext,
|
||||
getHighlightComment,
|
||||
getHighlightSourceEventPointer,
|
||||
getHighlightSourceAddressPointer,
|
||||
getHighlightSourceUrl,
|
||||
getHighlightAttributions
|
||||
} from 'applesauce-core/helpers'
|
||||
|
||||
/**
|
||||
* Creates and publishes a highlight event (NIP-84)
|
||||
* Supports both nostr-native articles and external URLs
|
||||
* Returns the signed event for immediate UI updates
|
||||
*/
|
||||
export async function createHighlight(
|
||||
selectedText: string,
|
||||
@@ -17,7 +28,7 @@ export async function createHighlight(
|
||||
relayPool: RelayPool,
|
||||
contentForContext?: string,
|
||||
comment?: string
|
||||
): Promise<void> {
|
||||
): Promise<NostrEvent> {
|
||||
if (!selectedText || !source) {
|
||||
throw new Error('Missing required data to create highlight')
|
||||
}
|
||||
@@ -65,6 +76,9 @@ export async function createHighlight(
|
||||
await relayPool.publish(RELAYS, signedEvent)
|
||||
|
||||
console.log('✅ Highlight published to', RELAYS.length, 'relays (including local):', signedEvent)
|
||||
|
||||
// Return the signed event for immediate UI updates
|
||||
return signedEvent
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,3 +173,33 @@ function extractContext(selectedText: string, articleContent: string): string |
|
||||
return contextParts.length > 1 ? contextParts.join(' ') : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a NostrEvent to a Highlight object for immediate UI display
|
||||
*/
|
||||
export function eventToHighlight(event: NostrEvent): Highlight {
|
||||
const highlightText = getHighlightText(event)
|
||||
const context = getHighlightContext(event)
|
||||
const comment = getHighlightComment(event)
|
||||
const sourceEventPointer = getHighlightSourceEventPointer(event)
|
||||
const sourceAddressPointer = getHighlightSourceAddressPointer(event)
|
||||
const sourceUrl = getHighlightSourceUrl(event)
|
||||
const attributions = getHighlightAttributions(event)
|
||||
|
||||
const author = attributions.find(a => a.role === 'author')?.pubkey
|
||||
const eventReference = sourceEventPointer?.id ||
|
||||
(sourceAddressPointer ? `${sourceAddressPointer.kind}:${sourceAddressPointer.pubkey}:${sourceAddressPointer.identifier}` : undefined)
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
pubkey: event.pubkey,
|
||||
created_at: event.created_at,
|
||||
content: highlightText,
|
||||
tags: event.tags,
|
||||
eventReference,
|
||||
urlReference: sourceUrl,
|
||||
author,
|
||||
context,
|
||||
comment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user