mirror of
https://github.com/dergigi/boris.git
synced 2025-12-24 01:54:19 +01:00
feat: instant mark-as-read with checkmark animation and read status checking
- Add functions to check if article/URL was already marked as read via NIP-25 reactions - Make mark-as-read action instant with fire-and-forget publishing - Add checkmark icon animation when marking as read - Display read status on load by querying kind:7 (nostr events) and kind:17 (websites) reactions - Add green styling for already-read state - Button shows checkmark and is disabled when article is already marked as read
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useMemo, useState, useEffect } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSpinner, faBook } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faSpinner, faBook, faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { IAccount } from 'applesauce-accounts'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
@@ -15,7 +15,12 @@ import { useMarkdownToHTML } from '../hooks/useMarkdownToHTML'
|
||||
import { useHighlightedContent } from '../hooks/useHighlightedContent'
|
||||
import { useHighlightInteractions } from '../hooks/useHighlightInteractions'
|
||||
import { UserSettings } from '../services/settingsService'
|
||||
import { createEventReaction, createWebsiteReaction } from '../services/reactionService'
|
||||
import {
|
||||
createEventReaction,
|
||||
createWebsiteReaction,
|
||||
hasMarkedEventAsRead,
|
||||
hasMarkedWebsiteAsRead
|
||||
} from '../services/reactionService'
|
||||
import AuthorCard from './AuthorCard'
|
||||
|
||||
interface ContentPanelProps {
|
||||
@@ -70,7 +75,9 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
onTextSelection,
|
||||
onClearSelection
|
||||
}) => {
|
||||
const [isMarkingAsRead, setIsMarkingAsRead] = useState(false)
|
||||
const [isMarkedAsRead, setIsMarkedAsRead] = useState(false)
|
||||
const [isCheckingReadStatus, setIsCheckingReadStatus] = useState(false)
|
||||
const [showCheckAnimation, setShowCheckAnimation] = useState(false)
|
||||
const { renderedHtml: renderedMarkdownHtml, previewRef: markdownPreviewRef, processedMarkdown } = useMarkdownToHTML(markdown, relayPool)
|
||||
|
||||
const { finalHtml, relevantHighlights } = useHighlightedContent({
|
||||
@@ -105,39 +112,82 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
// Determine if we're on a nostr-native article (/a/) or external URL (/r/)
|
||||
const isNostrArticle = selectedUrl && selectedUrl.startsWith('nostr:')
|
||||
|
||||
const handleMarkAsRead = async () => {
|
||||
if (!activeAccount || !relayPool) {
|
||||
console.warn('Cannot mark as read: no account or relay pool')
|
||||
// Check if article is already marked as read when URL/article changes
|
||||
useEffect(() => {
|
||||
const checkReadStatus = async () => {
|
||||
if (!activeAccount || !relayPool || !selectedUrl) {
|
||||
setIsMarkedAsRead(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsCheckingReadStatus(true)
|
||||
|
||||
try {
|
||||
let hasRead = false
|
||||
if (isNostrArticle && currentArticle) {
|
||||
hasRead = await hasMarkedEventAsRead(
|
||||
currentArticle.id,
|
||||
activeAccount.pubkey,
|
||||
relayPool
|
||||
)
|
||||
} else {
|
||||
hasRead = await hasMarkedWebsiteAsRead(
|
||||
selectedUrl,
|
||||
activeAccount.pubkey,
|
||||
relayPool
|
||||
)
|
||||
}
|
||||
setIsMarkedAsRead(hasRead)
|
||||
} catch (error) {
|
||||
console.error('Failed to check read status:', error)
|
||||
} finally {
|
||||
setIsCheckingReadStatus(false)
|
||||
}
|
||||
}
|
||||
|
||||
checkReadStatus()
|
||||
}, [selectedUrl, currentArticle?.id, activeAccount, relayPool, isNostrArticle])
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
if (!activeAccount || !relayPool || isMarkedAsRead) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsMarkingAsRead(true)
|
||||
// Instantly update UI with checkmark animation
|
||||
setIsMarkedAsRead(true)
|
||||
setShowCheckAnimation(true)
|
||||
|
||||
try {
|
||||
if (isNostrArticle && currentArticle) {
|
||||
// Kind 7 reaction for nostr-native articles
|
||||
await createEventReaction(
|
||||
currentArticle.id,
|
||||
currentArticle.pubkey,
|
||||
currentArticle.kind,
|
||||
activeAccount,
|
||||
relayPool
|
||||
)
|
||||
console.log('✅ Marked nostr article as read')
|
||||
} else if (selectedUrl) {
|
||||
// Kind 17 reaction for external websites
|
||||
await createWebsiteReaction(
|
||||
selectedUrl,
|
||||
activeAccount,
|
||||
relayPool
|
||||
)
|
||||
console.log('✅ Marked website as read')
|
||||
// Reset animation after it completes
|
||||
setTimeout(() => {
|
||||
setShowCheckAnimation(false)
|
||||
}, 600)
|
||||
|
||||
// Fire-and-forget: publish in background without blocking UI
|
||||
;(async () => {
|
||||
try {
|
||||
if (isNostrArticle && currentArticle) {
|
||||
await createEventReaction(
|
||||
currentArticle.id,
|
||||
currentArticle.pubkey,
|
||||
currentArticle.kind,
|
||||
activeAccount,
|
||||
relayPool
|
||||
)
|
||||
console.log('✅ Marked nostr article as read')
|
||||
} else if (selectedUrl) {
|
||||
await createWebsiteReaction(
|
||||
selectedUrl,
|
||||
activeAccount,
|
||||
relayPool
|
||||
)
|
||||
console.log('✅ Marked website as read')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to mark as read:', error)
|
||||
// Revert UI state on error
|
||||
setIsMarkedAsRead(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to mark as read:', error)
|
||||
} finally {
|
||||
setIsMarkingAsRead(false)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
if (!selectedUrl) {
|
||||
@@ -215,13 +265,18 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
{activeAccount && (
|
||||
<div className="mark-as-read-container">
|
||||
<button
|
||||
className="mark-as-read-btn"
|
||||
className={`mark-as-read-btn ${isMarkedAsRead ? 'marked' : ''} ${showCheckAnimation ? 'animating' : ''}`}
|
||||
onClick={handleMarkAsRead}
|
||||
disabled={isMarkingAsRead}
|
||||
title="Mark as Read"
|
||||
disabled={isMarkedAsRead || isCheckingReadStatus}
|
||||
title={isMarkedAsRead ? 'Already Marked as Read' : 'Mark as Read'}
|
||||
>
|
||||
<FontAwesomeIcon icon={isMarkingAsRead ? faSpinner : faBook} spin={isMarkingAsRead} />
|
||||
<span>{isMarkingAsRead ? 'Marking...' : 'Mark as Read'}</span>
|
||||
<FontAwesomeIcon
|
||||
icon={isCheckingReadStatus ? faSpinner : isMarkedAsRead ? faCheck : faBook}
|
||||
spin={isCheckingReadStatus}
|
||||
/>
|
||||
<span>
|
||||
{isCheckingReadStatus ? 'Checking...' : isMarkedAsRead ? 'Marked as Read' : 'Mark as Read'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user