feat: track mark-as-read reactions in readingProgressController

Extended readingProgressController to also fetch and track mark-as-read
reactions (kind:7 and kind:17 with MARK_AS_READ_EMOJI) alongside reading
progress events.

Changes:
- Added markedAsReadIds Set to controller
- Query mark-as-read reactions in parallel with reading progress
- Added isMarkedAsRead() method to check if article is marked as read
- Updated Me.tsx to include markedAsRead status in ReadItems

Now /me/reads/completed shows:
- Articles with >= 95% reading progress
- Articles marked as read with the 📚 emoji
This commit is contained in:
Gigi
2025-10-19 23:33:22 +02:00
parent 5633dc640c
commit 23b4c3475f
2 changed files with 53 additions and 1 deletions

View File

@@ -157,7 +157,20 @@ const Me: React.FC<MeProps> = ({
setReadingProgressMap(readingProgressController.getProgressMap())
// Subscribe to updates
const unsubProgress = readingProgressController.onProgress(setReadingProgressMap)
const unsubProgress = readingProgressController.onProgress((progressMap) => {
const readItems: ReadItem[] = Array.from(progressMap.entries()).map(([id, progress]) => ({
id,
source: 'reading-progress',
type: 'article',
readingProgress: progress,
markedAsRead: readingProgressController.isMarkedAsRead(id),
readingTimestamp: Math.floor(Date.now() / 1000)
}))
const readsMap = new Map(readItems.map(item => [item.id, item]))
setReadsMap(readsMap)
setReads(readItems)
})
return () => {
unsubProgress()
@@ -240,6 +253,7 @@ const Me: React.FC<MeProps> = ({
source: 'reading-progress',
type: 'article',
readingProgress: progress,
markedAsRead: readingProgressController.isMarkedAsRead(id),
readingTimestamp: Math.floor(Date.now() / 1000)
}))
@@ -264,6 +278,7 @@ const Me: React.FC<MeProps> = ({
source: 'reading-progress',
type: 'article',
readingProgress: progress,
markedAsRead: readingProgressController.isMarkedAsRead(id),
readingTimestamp: Math.floor(Date.now() / 1000)
}))

View File

@@ -6,6 +6,7 @@ import { KINDS } from '../config/kinds'
import { RELAYS } from '../config/relays'
import { processReadingProgress } from './readingDataProcessor'
import { ReadItem } from './readsService'
import { MARK_AS_READ_EMOJI } from './reactionService'
type ProgressMapCallback = (progressMap: Map<string, number>) => void
type LoadingCallback = (loading: boolean) => void
@@ -22,6 +23,7 @@ class ReadingProgressController {
private loadingListeners: LoadingCallback[] = []
private currentProgressMap: Map<string, number> = new Map()
private markedAsReadIds: Set<string> = new Set()
private lastLoadedPubkey: string | null = null
private generation = 0
private timelineSubscription: { unsubscribe: () => void } | null = null
@@ -91,6 +93,13 @@ class ReadingProgressController {
return this.currentProgressMap.get(naddr)
}
/**
* Check if article is marked as read
*/
isMarkedAsRead(naddr: string): boolean {
return this.markedAsReadIds.has(naddr)
}
/**
* Check if reading progress is loaded for a specific pubkey
*/
@@ -227,6 +236,34 @@ class ReadingProgressController {
const now = Math.floor(Date.now() / 1000)
this.updateLastSyncedAt(pubkey, now)
}
// Also fetch mark-as-read reactions in parallel
const [kind7Events, kind17Events] = await Promise.all([
queryEvents(relayPool, { kinds: [7], authors: [pubkey] }, { relayUrls: RELAYS }),
queryEvents(relayPool, { kinds: [17], authors: [pubkey] }, { relayUrls: RELAYS })
])
if (startGeneration !== this.generation) {
return
}
// Process mark-as-read reactions
;[...kind7Events, ...kind17Events].forEach((evt) => {
if (evt.content === MARK_AS_READ_EMOJI) {
// Extract article ID from tags
const eTag = evt.tags.find(t => t[0] === 'e')?.[1]
const rTag = evt.tags.find(t => t[0] === 'r')?.[1]
if (eTag) {
// For kind:7, look up the article from progress map by event ID
const articleId = Array.from(this.currentProgressMap.keys()).find(id => id.includes(eTag))
if (articleId) this.markedAsReadIds.add(articleId)
} else if (rTag) {
// For kind:17, the URL is the article ID
this.markedAsReadIds.add(rTag)
}
}
})
} catch (err) {
console.error('📊 [ReadingProgress] Failed to load:', err)
} finally {