mirror of
https://github.com/dergigi/boris.git
synced 2025-12-18 23:24:22 +01:00
feat: add auto-mark as read at 100% reading progress
- Add autoMarkAsReadAt100 setting (default: false) - Add checkbox in Layout & Behavior settings - Automatically mark article as read after 2 seconds at 100% progress - Trigger same animation as manual mark as read button - Move isNostrArticle computation earlier for useCallback deps - Move handleMarkAsRead to useCallback for use in auto-mark effect
This commit is contained in:
@@ -187,14 +187,76 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
|||||||
const { isReadingComplete, progressPercentage, saveNow } = useReadingPosition({
|
const { isReadingComplete, progressPercentage, saveNow } = useReadingPosition({
|
||||||
enabled: isTextContent,
|
enabled: isTextContent,
|
||||||
syncEnabled: settings?.syncReadingPosition,
|
syncEnabled: settings?.syncReadingPosition,
|
||||||
onSave: handleSavePosition,
|
onSave: handleSavePosition
|
||||||
onReadingComplete: () => {
|
})
|
||||||
// Optional: Auto-mark as read when reading is complete
|
|
||||||
if (activeAccount && !isMarkedAsRead) {
|
// Determine if we're on a nostr-native article (/a/) or external URL (/r/)
|
||||||
// Could trigger auto-mark as read here if desired
|
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
|
// Load saved reading position when article loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -330,8 +392,6 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
|||||||
|
|
||||||
const hasHighlights = relevantHighlights.length > 0
|
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)
|
const isExternalVideo = !isNostrArticle && !!selectedUrl && ['youtube', 'video'].includes(classifyUrl(selectedUrl).type)
|
||||||
|
|
||||||
// Track external video duration (in seconds) for display in header
|
// Track external video duration (in seconds) for display in header
|
||||||
@@ -600,48 +660,6 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
|||||||
|
|
||||||
checkReadStatus()
|
checkReadStatus()
|
||||||
}, [selectedUrl, currentArticle, activeAccount, relayPool, isNostrArticle])
|
}, [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) {
|
if (!selectedUrl) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -130,6 +130,19 @@ const LayoutBehaviorSettings: React.FC<LayoutBehaviorSettingsProps> = ({ setting
|
|||||||
<span>Auto-scroll to last reading position</span>
|
<span>Auto-scroll to last reading position</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="setting-group">
|
||||||
|
<label htmlFor="autoMarkAsReadAt100" className="checkbox-label">
|
||||||
|
<input
|
||||||
|
id="autoMarkAsReadAt100"
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.autoMarkAsReadAt100 ?? false}
|
||||||
|
onChange={(e) => onUpdate({ autoMarkAsReadAt100: e.target.checked })}
|
||||||
|
className="setting-checkbox"
|
||||||
|
/>
|
||||||
|
<span>Automatically mark as read when reading progress is 100%</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export interface UserSettings {
|
|||||||
// Reading position sync
|
// Reading position sync
|
||||||
syncReadingPosition?: boolean // default: false (opt-in)
|
syncReadingPosition?: boolean // default: false (opt-in)
|
||||||
autoScrollToPosition?: boolean // default: true (auto-scroll to last reading position)
|
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(
|
export async function loadSettings(
|
||||||
|
|||||||
Reference in New Issue
Block a user