diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index 962fc005..71975541 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -187,14 +187,76 @@ const ContentPanel: React.FC = ({ const { isReadingComplete, progressPercentage, saveNow } = useReadingPosition({ enabled: isTextContent, syncEnabled: settings?.syncReadingPosition, - onSave: handleSavePosition, - onReadingComplete: () => { - // Optional: Auto-mark as read when reading is complete - if (activeAccount && !isMarkedAsRead) { - // Could trigger auto-mark as read here if desired + onSave: handleSavePosition + }) + + // Determine if we're on a nostr-native article (/a/) or external URL (/r/) + const isNostrArticle = selectedUrl && selectedUrl.startsWith('nostr:') + + // Define handleMarkAsRead with useCallback to use in auto-mark effect + const handleMarkAsRead = useCallback(() => { + if (!activeAccount || !relayPool || isMarkedAsRead) { + return + } + + // Instantly update UI with checkmark animation + setIsMarkedAsRead(true) + setShowCheckAnimation(true) + + // 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) + } + })() + }, [activeAccount, relayPool, isMarkedAsRead, isNostrArticle, currentArticle, selectedUrl]) + + // Auto-mark as read when reaching 100% for 2 seconds + useEffect(() => { + if (!settings?.autoMarkAsReadAt100 || isMarkedAsRead || !activeAccount || !relayPool) { + return + } + + // Only trigger when progress is exactly 100% + if (progressPercentage === 100) { + console.log('📍 [ContentPanel] Progress at 100%, starting 2-second timer for auto-mark') + + const timer = setTimeout(() => { + console.log('✅ [ContentPanel] Auto-marking as read after 2 seconds at 100%') + handleMarkAsRead() + }, 2000) + + return () => { + console.log('âšī¸ [ContentPanel] Canceling auto-mark timer (progress changed or unmounting)') + clearTimeout(timer) } } - }) + }, [progressPercentage, settings?.autoMarkAsReadAt100, isMarkedAsRead, activeAccount, relayPool, handleMarkAsRead]) // Load saved reading position when article loads useEffect(() => { @@ -330,8 +392,6 @@ const ContentPanel: React.FC = ({ const hasHighlights = relevantHighlights.length > 0 - // Determine if we're on a nostr-native article (/a/) or external URL (/r/) - const isNostrArticle = selectedUrl && selectedUrl.startsWith('nostr:') const isExternalVideo = !isNostrArticle && !!selectedUrl && ['youtube', 'video'].includes(classifyUrl(selectedUrl).type) // Track external video duration (in seconds) for display in header @@ -600,48 +660,6 @@ const ContentPanel: React.FC = ({ checkReadStatus() }, [selectedUrl, currentArticle, activeAccount, relayPool, isNostrArticle]) - - const handleMarkAsRead = () => { - if (!activeAccount || !relayPool || isMarkedAsRead) { - return - } - - // Instantly update UI with checkmark animation - setIsMarkedAsRead(true) - setShowCheckAnimation(true) - - // 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) - } - })() - } if (!selectedUrl) { return ( diff --git a/src/components/Settings/LayoutBehaviorSettings.tsx b/src/components/Settings/LayoutBehaviorSettings.tsx index b62169fb..847ed9d2 100644 --- a/src/components/Settings/LayoutBehaviorSettings.tsx +++ b/src/components/Settings/LayoutBehaviorSettings.tsx @@ -130,6 +130,19 @@ const LayoutBehaviorSettings: React.FC = ({ setting Auto-scroll to last reading position + +
+ +
) } diff --git a/src/services/settingsService.ts b/src/services/settingsService.ts index 8f3fbed9..ed5edcff 100644 --- a/src/services/settingsService.ts +++ b/src/services/settingsService.ts @@ -57,6 +57,7 @@ export interface UserSettings { // Reading position sync syncReadingPosition?: boolean // default: false (opt-in) autoScrollToPosition?: boolean // default: true (auto-scroll to last reading position) + autoMarkAsReadAt100?: boolean // default: false (auto-mark as read when reaching 100% for 2 seconds) } export async function loadSettings(