feat: enable clicking on kind:30023 articles to open in reader

- Update handleSelectUrl to detect kind:30023 bookmarks
- Construct naddr from article event data (pubkey, d tag)
- Fetch and render articles using article service
- Update all bookmark views (Compact, Card, Large) to handle articles
- Show 'Read Article' button for kind:30023 bookmarks
- Articles load in the existing ContentPanel with full reader features
This commit is contained in:
Gigi
2025-10-05 08:19:50 +01:00
parent d5e847e515
commit 0f7a4d7877
8 changed files with 87 additions and 26 deletions

View File

@@ -0,0 +1,3 @@
---
alwaysApply: true
---

2
dist/index.html vendored
View File

@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Boris - Nostr Bookmarks</title>
<script type="module" crossorigin src="/assets/index-BldIeAhn.js"></script>
<script type="module" crossorigin src="/assets/index-Be3omHES.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-ChEoItgK.css">
</head>
<body>

View File

@@ -15,7 +15,7 @@ import { CardView } from './BookmarkViews/CardView'
interface BookmarkItemProps {
bookmark: IndividualBookmark
index: number
onSelectUrl?: (url: string) => void
onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][] }) => void
viewMode?: ViewMode
}
@@ -68,10 +68,20 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
}
const handleReadNow = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
// For kind:30023 articles, pass the bookmark data instead of URL
if (bookmark.kind === 30023) {
if (onSelectUrl) {
onSelectUrl('', { id: bookmark.id, kind: bookmark.kind, tags: bookmark.tags })
}
return
}
// For regular bookmarks with URLs
if (!hasUrls) return
const firstUrl = extractedUrls[0]
if (onSelectUrl) {
event.preventDefault()
onSelectUrl(firstUrl)
} else {
window.open(firstUrl, '_blank')

View File

@@ -9,7 +9,7 @@ import { ViewMode } from './Bookmarks'
interface BookmarkListProps {
bookmarks: Bookmark[]
onSelectUrl?: (url: string) => void
onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][] }) => void
isCollapsed: boolean
onToggleCollapse: () => void
onLogout: () => void

View File

@@ -13,7 +13,7 @@ interface CardViewProps {
index: number
hasUrls: boolean
extractedUrls: string[]
onSelectUrl?: (url: string) => void
onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][] }) => void
getIconForUrlType: IconGetter
firstUrlClassification: { buttonText: string } | null
authorNpub: string
@@ -141,11 +141,11 @@ export const CardView: React.FC<CardViewProps> = ({
{getAuthorDisplayName()}
</a>
</div>
{hasUrls && firstUrlClassification && (
{(hasUrls && firstUrlClassification) || bookmark.kind === 30023 ? (
<button className="read-now-button-minimal" onClick={handleReadNow}>
{firstUrlClassification.buttonText}
{bookmark.kind === 30023 ? 'Read Article' : firstUrlClassification?.buttonText}
</button>
)}
) : null}
</div>
</div>
)

View File

@@ -11,7 +11,7 @@ interface CompactViewProps {
index: number
hasUrls: boolean
extractedUrls: string[]
onSelectUrl?: (url: string) => void
onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][] }) => void
getIconForUrlType: IconGetter
firstUrlClassification: { buttonText: string } | null
}
@@ -25,8 +25,15 @@ export const CompactView: React.FC<CompactViewProps> = ({
getIconForUrlType,
firstUrlClassification
}) => {
const isArticle = bookmark.kind === 30023
const isClickable = hasUrls || isArticle
const handleCompactClick = () => {
if (hasUrls && onSelectUrl) {
if (!onSelectUrl) return
if (isArticle) {
onSelectUrl('', { id: bookmark.id, kind: bookmark.kind, tags: bookmark.tags })
} else if (hasUrls) {
onSelectUrl(extractedUrls[0])
}
}
@@ -34,10 +41,10 @@ export const CompactView: React.FC<CompactViewProps> = ({
return (
<div key={`${bookmark.id}-${index}`} className={`individual-bookmark compact ${bookmark.isPrivate ? 'private-bookmark' : ''}`}>
<div
className={`compact-row ${hasUrls ? 'clickable' : ''}`}
className={`compact-row ${isClickable ? 'clickable' : ''}`}
onClick={handleCompactClick}
role={hasUrls ? 'button' : undefined}
tabIndex={hasUrls ? 0 : undefined}
role={isClickable ? 'button' : undefined}
tabIndex={isClickable ? 0 : undefined}
>
<span className="bookmark-type-compact">
{bookmark.isPrivate ? (
@@ -55,13 +62,20 @@ export const CompactView: React.FC<CompactViewProps> = ({
</div>
)}
<span className="bookmark-date-compact">{formatDate(bookmark.created_at)}</span>
{hasUrls && (
{isClickable && (
<button
className="compact-read-btn"
onClick={(e) => { e.stopPropagation(); onSelectUrl?.(extractedUrls[0]) }}
title={firstUrlClassification?.buttonText}
onClick={(e) => {
e.stopPropagation()
if (isArticle) {
onSelectUrl?.('', { id: bookmark.id, kind: bookmark.kind, tags: bookmark.tags })
} else {
onSelectUrl?.(extractedUrls[0])
}
}}
title={isArticle ? 'Read Article' : firstUrlClassification?.buttonText}
>
<FontAwesomeIcon icon={getIconForUrlType(extractedUrls[0])} />
<FontAwesomeIcon icon={isArticle ? getIconForUrlType('') : getIconForUrlType(extractedUrls[0])} />
</button>
)}
</div>

View File

@@ -10,7 +10,7 @@ interface LargeViewProps {
index: number
hasUrls: boolean
extractedUrls: string[]
onSelectUrl?: (url: string) => void
onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][] }) => void
getIconForUrlType: IconGetter
firstUrlClassification: { buttonText: string } | null
previewImage: string | null
@@ -34,6 +34,8 @@ export const LargeView: React.FC<LargeViewProps> = ({
getAuthorDisplayName,
handleReadNow
}) => {
const isArticle = bookmark.kind === 30023
return (
<div key={`${bookmark.id}-${index}`} className={`individual-bookmark large ${bookmark.isPrivate ? 'private-bookmark' : ''}`}>
{hasUrls && (
@@ -80,12 +82,12 @@ export const LargeView: React.FC<LargeViewProps> = ({
</a>
)}
{hasUrls && firstUrlClassification && (
{(hasUrls && firstUrlClassification) || isArticle ? (
<button className="large-read-button" onClick={handleReadNow}>
<FontAwesomeIcon icon={getIconForUrlType(extractedUrls[0])} />
{firstUrlClassification.buttonText}
<FontAwesomeIcon icon={isArticle ? getIconForUrlType('') : getIconForUrlType(extractedUrls[0])} />
{isArticle ? 'Read Article' : firstUrlClassification?.buttonText}
</button>
)}
) : null}
</div>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { nip19 } from 'nostr-tools'
import { Hooks } from 'applesauce-react'
import { useEventStore } from 'applesauce-react/hooks'
import { RelayPool } from 'applesauce-relay'
@@ -113,17 +114,48 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
}
}
const handleSelectUrl = async (url: string) => {
const handleSelectUrl = async (url: string, bookmark?: { id: string; kind: number; tags: string[][] }) => {
if (!relayPool) return
setSelectedUrl(url)
setReaderLoading(true)
setReaderContent(undefined)
setShowSettings(false)
if (settings.collapseOnArticleOpen !== false) setIsCollapsed(true)
try {
const content = await fetchReadableContent(url)
setReaderContent(content)
// Check if this is a kind:30023 article
if (bookmark && bookmark.kind === 30023) {
// For articles, construct an naddr and fetch using article service
const dTag = bookmark.tags.find(t => t[0] === 'd')?.[1] || ''
// Try to get author from tags first, then fall back to bookmark id as pubkey
const author = bookmark.tags.find(t => t[0] === 'author')?.[1] ||
(bookmark.id.length === 64 ? bookmark.id : undefined)
if (dTag !== undefined) {
const pointer = {
identifier: dTag,
kind: 30023,
pubkey: author || bookmark.id,
}
const naddr = nip19.naddrEncode(pointer)
const article = await fetchArticleByNaddr(relayPool, naddr)
setReaderContent({
title: article.title,
markdown: article.markdown,
url: `nostr:${naddr}`
})
} else {
throw new Error('Invalid article reference - missing d tag')
}
} else {
// For regular URLs, fetch readable content
const content = await fetchReadableContent(url)
setReaderContent(content)
}
} catch (err) {
console.warn('Failed to fetch readable content:', err)
console.warn('Failed to fetch content:', err)
} finally {
setReaderLoading(false)
}