mirror of
https://github.com/dergigi/boris.git
synced 2026-02-11 01:54:22 +01:00
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:
@@ -0,0 +1,3 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
2
dist/index.html
vendored
2
dist/index.html
vendored
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user