mirror of
https://github.com/dergigi/boris.git
synced 2025-12-23 17:44:19 +01:00
feat: treat marked-as-read articles as 100% progress
- Fetch marked-as-read articles in useBookmarksData and Explore - Pass markedAsReadIds through component chain (Bookmarks -> ThreePaneLayout -> BookmarkList) - Display 100% progress for marked articles in all views (Archive, Bookmarks, Explore) - Update filter logic to treat marked articles as completed - Marked articles show green 100% progress bar - Marked articles only appear in 'completed' or 'all' filters - Remove reading position tracking from Me.tsx (not needed when all are marked) - Clean up unused imports and variables
This commit is contained in:
@@ -41,6 +41,7 @@ interface BookmarkListProps {
|
|||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
settings?: UserSettings
|
settings?: UserSettings
|
||||||
readingPositions?: Map<string, number>
|
readingPositions?: Map<string, number>
|
||||||
|
markedAsReadIds?: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BookmarkList: React.FC<BookmarkListProps> = ({
|
export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||||
@@ -60,7 +61,8 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
|||||||
relayPool,
|
relayPool,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
settings,
|
settings,
|
||||||
readingPositions
|
readingPositions,
|
||||||
|
markedAsReadIds
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const bookmarksListRef = useRef<HTMLDivElement>(null)
|
const bookmarksListRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -105,18 +107,24 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
|||||||
// If reading progress filter is 'all', show all articles
|
// If reading progress filter is 'all', show all articles
|
||||||
if (readingProgressFilter === 'all') return true
|
if (readingProgressFilter === 'all') return true
|
||||||
|
|
||||||
|
const isMarkedAsRead = markedAsReadIds?.has(bookmark.id)
|
||||||
const position = readingPositions?.get(bookmark.id)
|
const position = readingPositions?.get(bookmark.id)
|
||||||
|
|
||||||
|
// Marked-as-read articles are always treated as 100% complete
|
||||||
|
if (isMarkedAsRead) {
|
||||||
|
return readingProgressFilter === 'completed'
|
||||||
|
}
|
||||||
|
|
||||||
switch (readingProgressFilter) {
|
switch (readingProgressFilter) {
|
||||||
case 'to-read':
|
case 'to-read':
|
||||||
// 0-5% reading progress (has tracking data, not manually marked)
|
// 0-5% reading progress (has tracking data)
|
||||||
return position !== undefined && position >= 0 && position <= 0.05
|
return position !== undefined && position >= 0 && position <= 0.05
|
||||||
case 'reading':
|
case 'reading':
|
||||||
// Has some progress but not completed (5% < position < 95%)
|
// Has some progress but not completed (5% < position < 95%)
|
||||||
return position !== undefined && position > 0.05 && position < 0.95
|
return position !== undefined && position > 0.05 && position < 0.95
|
||||||
case 'completed':
|
case 'completed':
|
||||||
// 95% or more read, OR manually marked as read (no position data or 0%)
|
// 95% or more read
|
||||||
return (position !== undefined && position >= 0.95) || !position || position === 0
|
return position !== undefined && position >= 0.95
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -233,7 +241,7 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
|||||||
index={index}
|
index={index}
|
||||||
onSelectUrl={onSelectUrl}
|
onSelectUrl={onSelectUrl}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
readingProgress={readingPositions?.get(individualBookmark.id)}
|
readingProgress={markedAsReadIds?.has(individualBookmark.id) ? 1.0 : readingPositions?.get(individualBookmark.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -162,7 +162,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
lastFetchTime,
|
lastFetchTime,
|
||||||
handleFetchHighlights,
|
handleFetchHighlights,
|
||||||
handleRefreshAll,
|
handleRefreshAll,
|
||||||
readingPositions
|
readingPositions,
|
||||||
|
markedAsReadIds
|
||||||
} = useBookmarksData({
|
} = useBookmarksData({
|
||||||
relayPool,
|
relayPool,
|
||||||
activeAccount,
|
activeAccount,
|
||||||
@@ -315,6 +316,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
onCreateHighlight={handleCreateHighlight}
|
onCreateHighlight={handleCreateHighlight}
|
||||||
hasActiveAccount={!!(activeAccount && relayPool)}
|
hasActiveAccount={!!(activeAccount && relayPool)}
|
||||||
readingPositions={readingPositions}
|
readingPositions={readingPositions}
|
||||||
|
markedAsReadIds={markedAsReadIds}
|
||||||
explore={showExplore ? (
|
explore={showExplore ? (
|
||||||
relayPool ? <Explore relayPool={relayPool} eventStore={eventStore} settings={settings} activeTab={exploreTab} /> : null
|
relayPool ? <Explore relayPool={relayPool} eventStore={eventStore} settings={settings} activeTab={exploreTab} /> : null
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import RefreshIndicator from './RefreshIndicator'
|
|||||||
import { classifyHighlights } from '../utils/highlightClassification'
|
import { classifyHighlights } from '../utils/highlightClassification'
|
||||||
import { HighlightVisibility } from './HighlightsPanel'
|
import { HighlightVisibility } from './HighlightsPanel'
|
||||||
import { loadReadingPosition, generateArticleIdentifier } from '../services/readingPositionService'
|
import { loadReadingPosition, generateArticleIdentifier } from '../services/readingPositionService'
|
||||||
|
import { fetchReadArticles } from '../services/libraryService'
|
||||||
|
|
||||||
interface ExploreProps {
|
interface ExploreProps {
|
||||||
relayPool: RelayPool
|
relayPool: RelayPool
|
||||||
@@ -43,6 +44,7 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
||||||
const [readingPositions, setReadingPositions] = useState<Map<string, number>>(new Map())
|
const [readingPositions, setReadingPositions] = useState<Map<string, number>>(new Map())
|
||||||
|
const [markedAsReadIds, setMarkedAsReadIds] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
// Visibility filters (defaults from settings, or friends only)
|
// Visibility filters (defaults from settings, or friends only)
|
||||||
const [visibility, setVisibility] = useState<HighlightVisibility>({
|
const [visibility, setVisibility] = useState<HighlightVisibility>({
|
||||||
@@ -215,6 +217,25 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
|
|||||||
loadData()
|
loadData()
|
||||||
}, [relayPool, activeAccount, refreshTrigger, eventStore, settings])
|
}, [relayPool, activeAccount, refreshTrigger, eventStore, settings])
|
||||||
|
|
||||||
|
// Fetch marked-as-read articles
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMarkedAsRead = async () => {
|
||||||
|
if (!activeAccount) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const readArticles = await fetchReadArticles(relayPool, activeAccount.pubkey)
|
||||||
|
const ids = new Set(readArticles.map(article => article.id))
|
||||||
|
setMarkedAsReadIds(ids)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ [Explore] Failed to load marked-as-read articles:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMarkedAsRead()
|
||||||
|
}, [relayPool, activeAccount])
|
||||||
|
|
||||||
// Load reading positions for blog posts
|
// Load reading positions for blog posts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPositions = async () => {
|
const loadPositions = async () => {
|
||||||
@@ -347,7 +368,7 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
|
|||||||
post={post}
|
post={post}
|
||||||
href={getPostUrl(post)}
|
href={getPostUrl(post)}
|
||||||
level={post.level}
|
level={post.level}
|
||||||
readingProgress={readingPositions.get(post.event.id)}
|
readingProgress={markedAsReadIds.has(post.event.id) ? 1.0 : readingPositions.get(post.event.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import RefreshIndicator from './RefreshIndicator'
|
|||||||
import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils'
|
import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils'
|
||||||
import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters'
|
import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters'
|
||||||
import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier'
|
import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier'
|
||||||
import { generateArticleIdentifier, loadReadingPosition } from '../services/readingPositionService'
|
|
||||||
import ReadingProgressFilters, { ReadingProgressFilterType } from './ReadingProgressFilters'
|
import ReadingProgressFilters, { ReadingProgressFilterType } from './ReadingProgressFilters'
|
||||||
|
|
||||||
interface MeProps {
|
interface MeProps {
|
||||||
@@ -39,7 +38,6 @@ type TabType = 'highlights' | 'reading-list' | 'archive' | 'writings'
|
|||||||
|
|
||||||
const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: propPubkey }) => {
|
const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: propPubkey }) => {
|
||||||
const activeAccount = Hooks.useActiveAccount()
|
const activeAccount = Hooks.useActiveAccount()
|
||||||
const eventStore = Hooks.useEventStore()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [activeTab, setActiveTab] = useState<TabType>(propActiveTab || 'highlights')
|
const [activeTab, setActiveTab] = useState<TabType>(propActiveTab || 'highlights')
|
||||||
|
|
||||||
@@ -55,7 +53,6 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
|
|||||||
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
||||||
const [bookmarkFilter, setBookmarkFilter] = useState<BookmarkFilterType>('all')
|
const [bookmarkFilter, setBookmarkFilter] = useState<BookmarkFilterType>('all')
|
||||||
const [readingProgressFilter, setReadingProgressFilter] = useState<ReadingProgressFilterType>('all')
|
const [readingProgressFilter, setReadingProgressFilter] = useState<ReadingProgressFilterType>('all')
|
||||||
const [readingPositions, setReadingPositions] = useState<Map<string, number>>(new Map())
|
|
||||||
|
|
||||||
// Update local state when prop changes
|
// Update local state when prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -127,64 +124,6 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
|
|||||||
loadData()
|
loadData()
|
||||||
}, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger])
|
}, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger])
|
||||||
|
|
||||||
// Load reading positions for read articles (only for own profile)
|
|
||||||
useEffect(() => {
|
|
||||||
const loadPositions = async () => {
|
|
||||||
if (!isOwnProfile || !activeAccount || !relayPool || !eventStore || readArticles.length === 0) {
|
|
||||||
console.log('🔍 [Archive] Skipping position load:', {
|
|
||||||
isOwnProfile,
|
|
||||||
hasAccount: !!activeAccount,
|
|
||||||
hasRelayPool: !!relayPool,
|
|
||||||
hasEventStore: !!eventStore,
|
|
||||||
articlesCount: readArticles.length
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('📊 [Archive] Loading reading positions for', readArticles.length, 'articles')
|
|
||||||
|
|
||||||
const positions = new Map<string, number>()
|
|
||||||
|
|
||||||
// Load positions for all read articles
|
|
||||||
await Promise.all(
|
|
||||||
readArticles.map(async (post) => {
|
|
||||||
try {
|
|
||||||
const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] || ''
|
|
||||||
const naddr = nip19.naddrEncode({
|
|
||||||
kind: 30023,
|
|
||||||
pubkey: post.author,
|
|
||||||
identifier: dTag
|
|
||||||
})
|
|
||||||
const articleUrl = `nostr:${naddr}`
|
|
||||||
const identifier = generateArticleIdentifier(articleUrl)
|
|
||||||
|
|
||||||
console.log('🔍 [Archive] Loading position for:', post.title?.slice(0, 50), 'identifier:', identifier.slice(0, 32))
|
|
||||||
|
|
||||||
const savedPosition = await loadReadingPosition(
|
|
||||||
relayPool,
|
|
||||||
eventStore,
|
|
||||||
activeAccount.pubkey,
|
|
||||||
identifier
|
|
||||||
)
|
|
||||||
|
|
||||||
if (savedPosition && savedPosition.position > 0) {
|
|
||||||
console.log('✅ [Archive] Found position:', Math.round(savedPosition.position * 100) + '%', 'for', post.title?.slice(0, 50))
|
|
||||||
positions.set(post.event.id, savedPosition.position)
|
|
||||||
} else {
|
|
||||||
console.log('❌ [Archive] No position found for:', post.title?.slice(0, 50))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('⚠️ [Archive] Failed to load reading position for article:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('📊 [Archive] Loaded positions for', positions.size, '/', readArticles.length, 'articles')
|
|
||||||
setReadingPositions(positions)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPositions()
|
|
||||||
}, [readArticles, isOwnProfile, activeAccount, relayPool, eventStore])
|
|
||||||
|
|
||||||
// Pull-to-refresh
|
// Pull-to-refresh
|
||||||
const { isRefreshing, pullPosition } = usePullToRefresh({
|
const { isRefreshing, pullPosition } = usePullToRefresh({
|
||||||
@@ -246,19 +185,21 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
|
|||||||
const groups = groupIndividualBookmarks(filteredBookmarks)
|
const groups = groupIndividualBookmarks(filteredBookmarks)
|
||||||
|
|
||||||
// Apply reading progress filter
|
// Apply reading progress filter
|
||||||
const filteredReadArticles = readArticles.filter(post => {
|
const filteredReadArticles = readArticles.filter(() => {
|
||||||
const position = readingPositions.get(post.event.id)
|
// All articles in readArticles are marked as read, so they're treated as 100% complete
|
||||||
|
// The filters are only useful for distinguishing between different completion states
|
||||||
|
// but since these are all marked as read, we only care about the 'all' and 'completed' filters
|
||||||
|
|
||||||
switch (readingProgressFilter) {
|
switch (readingProgressFilter) {
|
||||||
case 'to-read':
|
case 'to-read':
|
||||||
// 0-5% reading progress (has tracking data, not manually marked)
|
// Marked articles are never "to-read"
|
||||||
return position !== undefined && position >= 0 && position <= 0.05
|
return false
|
||||||
case 'reading':
|
case 'reading':
|
||||||
// Has some progress but not completed (5% < position < 95%)
|
// Marked articles are never "in progress"
|
||||||
return position !== undefined && position > 0.05 && position < 0.95
|
return false
|
||||||
case 'completed':
|
case 'completed':
|
||||||
// 95% or more read, OR manually marked as read (no position data or 0%)
|
// All marked articles are considered completed
|
||||||
return (position !== undefined && position >= 0.95) || !position || position === 0
|
return true
|
||||||
case 'all':
|
case 'all':
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
@@ -415,7 +356,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
|
|||||||
key={post.event.id}
|
key={post.event.id}
|
||||||
post={post}
|
post={post}
|
||||||
href={getPostUrl(post)}
|
href={getPostUrl(post)}
|
||||||
readingProgress={readingPositions.get(post.event.id)}
|
readingProgress={1.0}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ interface ThreePaneLayoutProps {
|
|||||||
relayPool: RelayPool | null
|
relayPool: RelayPool | null
|
||||||
eventStore: IEventStore | null
|
eventStore: IEventStore | null
|
||||||
readingPositions?: Map<string, number>
|
readingPositions?: Map<string, number>
|
||||||
|
markedAsReadIds?: Set<string>
|
||||||
|
|
||||||
// Content pane
|
// Content pane
|
||||||
readerLoading: boolean
|
readerLoading: boolean
|
||||||
@@ -326,6 +327,7 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
|||||||
relayPool={props.relayPool}
|
relayPool={props.relayPool}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
readingPositions={props.readingPositions}
|
readingPositions={props.readingPositions}
|
||||||
|
markedAsReadIds={props.markedAsReadIds}
|
||||||
settings={props.settings}
|
settings={props.settings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { fetchHighlights, fetchHighlightsForArticle } from '../services/highligh
|
|||||||
import { fetchContacts } from '../services/contactService'
|
import { fetchContacts } from '../services/contactService'
|
||||||
import { UserSettings } from '../services/settingsService'
|
import { UserSettings } from '../services/settingsService'
|
||||||
import { loadReadingPosition, generateArticleIdentifier } from '../services/readingPositionService'
|
import { loadReadingPosition, generateArticleIdentifier } from '../services/readingPositionService'
|
||||||
|
import { fetchReadArticles } from '../services/libraryService'
|
||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
interface UseBookmarksDataParams {
|
interface UseBookmarksDataParams {
|
||||||
@@ -42,6 +43,7 @@ export const useBookmarksData = ({
|
|||||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||||
const [lastFetchTime, setLastFetchTime] = useState<number | null>(null)
|
const [lastFetchTime, setLastFetchTime] = useState<number | null>(null)
|
||||||
const [readingPositions, setReadingPositions] = useState<Map<string, number>>(new Map())
|
const [readingPositions, setReadingPositions] = useState<Map<string, number>>(new Map())
|
||||||
|
const [markedAsReadIds, setMarkedAsReadIds] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
const handleFetchContacts = useCallback(async () => {
|
const handleFetchContacts = useCallback(async () => {
|
||||||
if (!relayPool || !activeAccount) return
|
if (!relayPool || !activeAccount) return
|
||||||
@@ -131,6 +133,25 @@ export const useBookmarksData = ({
|
|||||||
handleFetchContacts()
|
handleFetchContacts()
|
||||||
}, [relayPool, activeAccount, naddr, externalUrl, handleFetchHighlights, handleFetchContacts])
|
}, [relayPool, activeAccount, naddr, externalUrl, handleFetchHighlights, handleFetchContacts])
|
||||||
|
|
||||||
|
// Fetch marked-as-read articles
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMarkedAsRead = async () => {
|
||||||
|
if (!activeAccount || !relayPool) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const readArticles = await fetchReadArticles(relayPool, activeAccount.pubkey)
|
||||||
|
const ids = new Set(readArticles.map(article => article.id))
|
||||||
|
setMarkedAsReadIds(ids)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ [Bookmarks] Failed to load marked-as-read articles:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMarkedAsRead()
|
||||||
|
}, [relayPool, activeAccount])
|
||||||
|
|
||||||
// Load reading positions for bookmarked articles (kind:30023)
|
// Load reading positions for bookmarked articles (kind:30023)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPositions = async () => {
|
const loadPositions = async () => {
|
||||||
@@ -192,7 +213,8 @@ export const useBookmarksData = ({
|
|||||||
handleFetchBookmarks,
|
handleFetchBookmarks,
|
||||||
handleFetchHighlights,
|
handleFetchHighlights,
|
||||||
handleRefreshAll,
|
handleRefreshAll,
|
||||||
readingPositions
|
readingPositions,
|
||||||
|
markedAsReadIds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user