feat: add highlight deletion with confirmation dialog

- Create deletionService for NIP-09 kind:5 event deletion requests
- Add ConfirmDialog component for user confirmation before deletion
- Add subtle delete button to highlight items (trash icon)
- Only show delete button for user's own highlights
- Position delete button symmetrically opposite to relay indicator
- Add confirmation dialog to prevent accidental deletions
- Remove highlights from UI immediately after deletion request
- Style delete button with red hover color
- Add comprehensive confirmation dialog styling (danger/warning/info variants)

Implements NIP-09 Event Deletion Request.
Users can now delete their own highlights after confirming the action.
This commit is contained in:
Gigi
2025-10-11 08:38:22 +01:00
parent 6a6b8c4fad
commit 0adb8d6766
6 changed files with 370 additions and 2 deletions

View File

@@ -0,0 +1,56 @@
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'
interface ConfirmDialogProps {
isOpen: boolean
title: string
message: string
confirmText?: string
cancelText?: string
onConfirm: () => void
onCancel: () => void
variant?: 'danger' | 'warning' | 'info'
}
const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
isOpen,
title,
message,
confirmText = 'Confirm',
cancelText = 'Cancel',
onConfirm,
onCancel,
variant = 'warning'
}) => {
if (!isOpen) return null
return (
<div className="confirm-dialog-overlay" onClick={onCancel}>
<div className="confirm-dialog" onClick={(e) => e.stopPropagation()}>
<div className={`confirm-dialog-icon ${variant}`}>
<FontAwesomeIcon icon={faExclamationTriangle} />
</div>
<h3 className="confirm-dialog-title">{title}</h3>
<p className="confirm-dialog-message">{message}</p>
<div className="confirm-dialog-actions">
<button
className="confirm-dialog-btn cancel"
onClick={onCancel}
>
{cancelText}
</button>
<button
className={`confirm-dialog-btn confirm ${variant}`}
onClick={onConfirm}
>
{confirmText}
</button>
</div>
</div>
</div>
)
}
export default ConfirmDialog

View File

@@ -1,15 +1,18 @@
import React, { useEffect, useRef, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer } from '@fortawesome/free-solid-svg-icons'
import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer, faTrash } from '@fortawesome/free-solid-svg-icons'
import { Highlight } from '../types/highlights'
import { useEventModel } from 'applesauce-react/hooks'
import { Models, IEventStore } from 'applesauce-core'
import { RelayPool } from 'applesauce-relay'
import { Hooks } from 'applesauce-react'
import { onSyncStateChange, isEventSyncing } from '../services/offlineSyncService'
import { RELAYS } from '../config/relays'
import { areAllRelaysLocal } from '../utils/helpers'
import { nip19 } from 'nostr-tools'
import { formatDateCompact } from '../utils/bookmarkUtils'
import { createDeletionRequest } from '../services/deletionService'
import ConfirmDialog from './ConfirmDialog'
interface HighlightWithLevel extends Highlight {
level?: 'mine' | 'friends' | 'nostrverse'
@@ -23,6 +26,7 @@ interface HighlightItemProps {
relayPool?: RelayPool | null
eventStore?: IEventStore | null
onHighlightUpdate?: (highlight: Highlight) => void
onHighlightDelete?: (highlightId: string) => void
}
export const HighlightItem: React.FC<HighlightItemProps> = ({
@@ -32,12 +36,17 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
onHighlightClick,
relayPool,
eventStore,
onHighlightUpdate
onHighlightUpdate,
onHighlightDelete
}) => {
const itemRef = useRef<HTMLDivElement>(null)
const [isSyncing, setIsSyncing] = useState(() => isEventSyncing(highlight.id))
const [showOfflineIndicator, setShowOfflineIndicator] = useState(() => highlight.isOfflineCreated && !isSyncing)
const [isRebroadcasting, setIsRebroadcasting] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const activeAccount = Hooks.useActiveAccount()
// Resolve the profile of the user who made the highlight
const profile = useEventModel(Models.ProfileModel, [highlight.pubkey])
@@ -243,7 +252,51 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
const relayIndicator = getRelayIndicatorInfo()
// Check if current user can delete this highlight
const canDelete = activeAccount && highlight.pubkey === activeAccount.pubkey
const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation()
setShowDeleteConfirm(true)
}
const handleConfirmDelete = async () => {
if (!activeAccount || !relayPool) {
console.warn('Cannot delete: no account or relay pool')
return
}
setIsDeleting(true)
setShowDeleteConfirm(false)
try {
await createDeletionRequest(
highlight.id,
9802, // kind for highlights
'Deleted by user',
activeAccount,
relayPool
)
console.log('✅ Highlight deletion request published')
// Notify parent to remove this highlight from the list
if (onHighlightDelete) {
onHighlightDelete(highlight.id)
}
} catch (error) {
console.error('Failed to delete highlight:', error)
} finally {
setIsDeleting(false)
}
}
const handleCancelDelete = () => {
setShowDeleteConfirm(false)
}
return (
<>
<div
ref={itemRef}
className={`highlight-item ${isSelected ? 'selected' : ''} ${highlight.level ? `level-${highlight.level}` : ''}`}
@@ -263,6 +316,15 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
<FontAwesomeIcon icon={relayIndicator.icon} spin={relayIndicator.spin} />
</div>
)}
{canDelete && (
<div
className="highlight-delete-btn"
title="Delete highlight"
onClick={handleDeleteClick}
>
<FontAwesomeIcon icon={isDeleting ? faSpinner : faTrash} spin={isDeleting} />
</div>
)}
</div>
<div className="highlight-content">
@@ -301,6 +363,18 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
</div>
</div>
</div>
<ConfirmDialog
isOpen={showDeleteConfirm}
title="Delete Highlight?"
message="This will request deletion of your highlight. It may still be visible on some relays that don't honor deletion requests."
confirmText="Delete"
cancelText="Cancel"
variant="danger"
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
/>
</>
)
}

View File

@@ -72,6 +72,11 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
)
}
const handleHighlightDelete = (highlightId: string) => {
// Remove highlight from local state
setLocalHighlights(prev => prev.filter(h => h.id !== highlightId))
}
const filteredHighlights = useFilteredHighlights({
highlights: localHighlights,
selectedUrl,
@@ -129,6 +134,7 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
relayPool={relayPool}
eventStore={eventStore}
onHighlightUpdate={handleHighlightUpdate}
onHighlightDelete={handleHighlightDelete}
/>
))}
</div>

View File

@@ -63,6 +63,11 @@ const Me: React.FC<MeProps> = ({ relayPool }) => {
loadHighlights()
}, [relayPool, activeAccount])
const handleHighlightDelete = (highlightId: string) => {
// Remove highlight from local state
setHighlights(prev => prev.filter(h => h.id !== highlightId))
}
if (loading) {
return (
<div className="explore-container">
@@ -102,6 +107,7 @@ const Me: React.FC<MeProps> = ({ relayPool }) => {
key={highlight.id}
highlight={highlight}
relayPool={relayPool}
onHighlightDelete={handleHighlightDelete}
/>
))}
</div>