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" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Boris - Nostr Bookmarks</title>
|
<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">
|
<link rel="stylesheet" crossorigin href="/assets/index-Bqz-n1DY.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Bookmark } from '../types/bookmarks'
|
|||||||
import { Highlight } from '../types/highlights'
|
import { Highlight } from '../types/highlights'
|
||||||
import { BookmarkList } from './BookmarkList'
|
import { BookmarkList } from './BookmarkList'
|
||||||
import { fetchBookmarks } from '../services/bookmarkService'
|
import { fetchBookmarks } from '../services/bookmarkService'
|
||||||
import { fetchHighlights, fetchHighlightsForArticle, fetchHighlightsForUrl } from '../services/highlightService'
|
import { fetchHighlights, fetchHighlightsForArticle } from '../services/highlightService'
|
||||||
import { fetchContacts } from '../services/contactService'
|
import { fetchContacts } from '../services/contactService'
|
||||||
import ContentPanel from './ContentPanel'
|
import ContentPanel from './ContentPanel'
|
||||||
import { HighlightsPanel } from './HighlightsPanel'
|
import { HighlightsPanel } from './HighlightsPanel'
|
||||||
@@ -20,7 +20,7 @@ import { useExternalUrlLoader } from '../hooks/useExternalUrlLoader'
|
|||||||
import { loadContent, BookmarkReference } from '../utils/contentLoader'
|
import { loadContent, BookmarkReference } from '../utils/contentLoader'
|
||||||
import { HighlightVisibility } from './HighlightsPanel'
|
import { HighlightVisibility } from './HighlightsPanel'
|
||||||
import { HighlightButton, HighlightButtonRef } from './HighlightButton'
|
import { HighlightButton, HighlightButtonRef } from './HighlightButton'
|
||||||
import { createHighlight } from '../services/highlightCreationService'
|
import { createHighlight, eventToHighlight } from '../services/highlightCreationService'
|
||||||
import { useRef, useCallback } from 'react'
|
import { useRef, useCallback } from 'react'
|
||||||
import { NostrEvent } from 'nostr-tools'
|
import { NostrEvent } from 'nostr-tools'
|
||||||
export type ViewMode = 'compact' | 'cards' | 'large'
|
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) => {
|
const handleTextSelection = useCallback((text: string) => {
|
||||||
highlightButtonRef.current?.updateSelection(text)
|
highlightButtonRef.current?.updateSelection(text)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -276,7 +252,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
? currentArticle.content
|
? currentArticle.content
|
||||||
: readerContent?.markdown || readerContent?.html
|
: readerContent?.markdown || readerContent?.html
|
||||||
|
|
||||||
await createHighlight(
|
// Create and publish the highlight
|
||||||
|
const signedEvent = await createHighlight(
|
||||||
text,
|
text,
|
||||||
source,
|
source,
|
||||||
activeAccount,
|
activeAccount,
|
||||||
@@ -287,12 +264,13 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
console.log('✅ Highlight created successfully!')
|
console.log('✅ Highlight created successfully!')
|
||||||
highlightButtonRef.current?.clearSelection()
|
highlightButtonRef.current?.clearSelection()
|
||||||
|
|
||||||
// Trigger refresh of highlights
|
// Immediately add the highlight to the UI (optimistic update)
|
||||||
handleHighlightCreated()
|
const newHighlight = eventToHighlight(signedEvent)
|
||||||
|
setHighlights(prev => [newHighlight, ...prev])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create highlight:', error)
|
console.error('Failed to create highlight:', error)
|
||||||
}
|
}
|
||||||
}, [activeAccount, relayPool, currentArticle, selectedUrl, readerContent, handleHighlightCreated])
|
}, [activeAccount, relayPool, currentArticle, selectedUrl, readerContent])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -5,10 +5,21 @@ import { IAccount } from 'applesauce-accounts'
|
|||||||
import { AddressPointer } from 'nostr-tools/nip19'
|
import { AddressPointer } from 'nostr-tools/nip19'
|
||||||
import { NostrEvent } from 'nostr-tools'
|
import { NostrEvent } from 'nostr-tools'
|
||||||
import { RELAYS } from '../config/relays'
|
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)
|
* Creates and publishes a highlight event (NIP-84)
|
||||||
* Supports both nostr-native articles and external URLs
|
* Supports both nostr-native articles and external URLs
|
||||||
|
* Returns the signed event for immediate UI updates
|
||||||
*/
|
*/
|
||||||
export async function createHighlight(
|
export async function createHighlight(
|
||||||
selectedText: string,
|
selectedText: string,
|
||||||
@@ -17,7 +28,7 @@ export async function createHighlight(
|
|||||||
relayPool: RelayPool,
|
relayPool: RelayPool,
|
||||||
contentForContext?: string,
|
contentForContext?: string,
|
||||||
comment?: string
|
comment?: string
|
||||||
): Promise<void> {
|
): Promise<NostrEvent> {
|
||||||
if (!selectedText || !source) {
|
if (!selectedText || !source) {
|
||||||
throw new Error('Missing required data to create highlight')
|
throw new Error('Missing required data to create highlight')
|
||||||
}
|
}
|
||||||
@@ -65,6 +76,9 @@ export async function createHighlight(
|
|||||||
await relayPool.publish(RELAYS, signedEvent)
|
await relayPool.publish(RELAYS, signedEvent)
|
||||||
|
|
||||||
console.log('✅ Highlight published to', RELAYS.length, 'relays (including local):', 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
|
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