mirror of
https://github.com/dergigi/boris.git
synced 2026-02-17 04:54:56 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59b7816312 | ||
|
|
3f1066ca71 | ||
|
|
744642e2b7 | ||
|
|
fd28a6e171 | ||
|
|
0124de8318 | ||
|
|
b37aac0a33 | ||
|
|
81ef047a31 | ||
|
|
704033e6cb | ||
|
|
d59d27419e |
2
dist/index.html
vendored
2
dist/index.html
vendored
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Boris - Nostr Bookmarks</title>
|
<title>Boris - Nostr Bookmarks</title>
|
||||||
<script type="module" crossorigin src="/assets/index-rEUBRPdE.js"></script>
|
<script type="module" crossorigin src="/assets/index-D55Gme04.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Bqz-n1DY.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Bqz-n1DY.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "boris",
|
"name": "boris",
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"description": "A minimal nostr client for bookmark management",
|
"description": "A minimal nostr client for bookmark management",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
|
|||||||
const firstUrl = hasUrls ? extractedUrls[0] : null
|
const firstUrl = hasUrls ? extractedUrls[0] : null
|
||||||
const firstUrlClassification = firstUrl ? classifyUrl(firstUrl) : null
|
const firstUrlClassification = firstUrl ? classifyUrl(firstUrl) : null
|
||||||
|
|
||||||
// For kind:30023 articles, extract image tag (per NIP-23)
|
// For kind:30023 articles, extract image and summary tags (per NIP-23)
|
||||||
// Note: We extract directly from tags here since we don't have the full event.
|
// Note: We extract directly from tags here since we don't have the full event.
|
||||||
// When we have full events, we use getArticleImage() helper (see articleService.ts)
|
// When we have full events, we use getArticleImage() helper (see articleService.ts)
|
||||||
const isArticle = bookmark.kind === 30023
|
const isArticle = bookmark.kind === 30023
|
||||||
const articleImage = isArticle ? bookmark.tags.find(t => t[0] === 'image')?.[1] : undefined
|
const articleImage = isArticle ? bookmark.tags.find(t => t[0] === 'image')?.[1] : undefined
|
||||||
|
const articleSummary = isArticle ? bookmark.tags.find(t => t[0] === 'summary')?.[1] : undefined
|
||||||
|
|
||||||
// Fetch OG image for large view (hook must be at top level)
|
// Fetch OG image for large view (hook must be at top level)
|
||||||
const instantPreview = firstUrl ? getPreviewImage(firstUrl, firstUrlClassification?.type || '') : null
|
const instantPreview = firstUrl ? getPreviewImage(firstUrl, firstUrlClassification?.type || '') : null
|
||||||
@@ -113,7 +114,8 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
|
|||||||
eventNevent,
|
eventNevent,
|
||||||
getAuthorDisplayName,
|
getAuthorDisplayName,
|
||||||
handleReadNow,
|
handleReadNow,
|
||||||
articleImage
|
articleImage,
|
||||||
|
articleSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewMode === 'compact') {
|
if (viewMode === 'compact') {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { faChevronLeft, faBookmark, faSpinner, faList, faThLarge, faImage } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronLeft, faBookmark, faSpinner, faList, faThLarge, faImage } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { Bookmark } from '../types/bookmarks'
|
import { Bookmark } from '../types/bookmarks'
|
||||||
import { BookmarkItem } from './BookmarkItem'
|
import { BookmarkItem } from './BookmarkItem'
|
||||||
import { formatDate, renderParsedContent } from '../utils/bookmarkUtils'
|
|
||||||
import SidebarHeader from './SidebarHeader'
|
import SidebarHeader from './SidebarHeader'
|
||||||
import IconButton from './IconButton'
|
import IconButton from './IconButton'
|
||||||
import { ViewMode } from './Bookmarks'
|
import { ViewMode } from './Bookmarks'
|
||||||
@@ -37,6 +36,12 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
|||||||
isRefreshing,
|
isRefreshing,
|
||||||
loading = false
|
loading = false
|
||||||
}) => {
|
}) => {
|
||||||
|
// Merge and flatten all individual bookmarks from all lists
|
||||||
|
// Re-sort after flattening to ensure newest first across all lists
|
||||||
|
const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || [])
|
||||||
|
.filter(ib => ib.content || ib.kind === 30023 || ib.kind === 39701)
|
||||||
|
.sort((a, b) => ((b.added_at || 0) - (a.added_at || 0)) || ((b.created_at || 0) - (a.created_at || 0)))
|
||||||
|
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
// Check if the selected URL is in bookmarks
|
// Check if the selected URL is in bookmarks
|
||||||
const isBookmarked = selectedUrl && bookmarks.some(bookmark => {
|
const isBookmarked = selectedUrl && bookmarks.some(bookmark => {
|
||||||
@@ -73,81 +78,24 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
|||||||
<div className="loading">
|
<div className="loading">
|
||||||
<FontAwesomeIcon icon={faSpinner} spin />
|
<FontAwesomeIcon icon={faSpinner} spin />
|
||||||
</div>
|
</div>
|
||||||
) : bookmarks.length === 0 ? (
|
) : allIndividualBookmarks.length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
<p>No bookmarks found.</p>
|
<p>No bookmarks found.</p>
|
||||||
<p>Add bookmarks using your nostr client to see them here.</p>
|
<p>Add bookmarks using your nostr client to see them here.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bookmarks-list">
|
<div className="bookmarks-list">
|
||||||
{bookmarks.map((bookmark, index) => (
|
<div className={`bookmarks-grid bookmarks-${viewMode}`}>
|
||||||
<div key={`${bookmark.id}-${index}`} className="bookmark-item">
|
{allIndividualBookmarks.map((individualBookmark, index) =>
|
||||||
{bookmark.bookmarkCount && (
|
<BookmarkItem
|
||||||
<p className="bookmark-count">
|
key={`${individualBookmark.id}-${index}`}
|
||||||
{bookmark.bookmarkCount} bookmarks in{' '}
|
bookmark={individualBookmark}
|
||||||
<a
|
index={index}
|
||||||
href={`https://search.dergigi.com/e/${bookmark.id}`}
|
onSelectUrl={onSelectUrl}
|
||||||
target="_blank"
|
viewMode={viewMode}
|
||||||
rel="noopener noreferrer"
|
/>
|
||||||
className="event-link"
|
)}
|
||||||
>
|
</div>
|
||||||
this list
|
|
||||||
</a>
|
|
||||||
:
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{bookmark.urlReferences && bookmark.urlReferences.length > 0 && (
|
|
||||||
<div className="bookmark-urls">
|
|
||||||
<h4>URLs:</h4>
|
|
||||||
{bookmark.urlReferences.map((url, index) => (
|
|
||||||
<a key={index} href={url} target="_blank" rel="noopener noreferrer" className="bookmark-url">
|
|
||||||
{url}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{bookmark.individualBookmarks && bookmark.individualBookmarks.length > 0 && (
|
|
||||||
<div className="individual-bookmarks">
|
|
||||||
<div className={`bookmarks-grid bookmarks-${viewMode}`}>
|
|
||||||
{bookmark.individualBookmarks.map((individualBookmark, index) =>
|
|
||||||
<BookmarkItem
|
|
||||||
key={index}
|
|
||||||
bookmark={individualBookmark}
|
|
||||||
index={index}
|
|
||||||
onSelectUrl={onSelectUrl}
|
|
||||||
viewMode={viewMode}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{bookmark.eventReferences && bookmark.eventReferences.length > 0 && bookmark.individualBookmarks?.length === 0 && (
|
|
||||||
<div className="bookmark-events">
|
|
||||||
<h4>Event References ({bookmark.eventReferences.length}):</h4>
|
|
||||||
<div className="event-ids">
|
|
||||||
{bookmark.eventReferences.slice(0, 3).map((eventId, index) => (
|
|
||||||
<span key={index} className="event-id">
|
|
||||||
{eventId.slice(0, 8)}...{eventId.slice(-8)}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{bookmark.eventReferences.length > 3 && (
|
|
||||||
<span className="more-events">... and {bookmark.eventReferences.length - 3} more</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{bookmark.parsedContent ? (
|
|
||||||
<div className="bookmark-content">
|
|
||||||
{renderParsedContent(bookmark.parsedContent)}
|
|
||||||
</div>
|
|
||||||
) : bookmark.content && (
|
|
||||||
<p className="bookmark-content">{bookmark.content}</p>
|
|
||||||
)}
|
|
||||||
<div className="bookmark-meta">
|
|
||||||
<span>Created: {formatDate(bookmark.created_at)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="view-mode-controls">
|
<div className="view-mode-controls">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface CardViewProps {
|
|||||||
getAuthorDisplayName: () => string
|
getAuthorDisplayName: () => string
|
||||||
handleReadNow: (e: React.MouseEvent<HTMLButtonElement>) => void
|
handleReadNow: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
articleImage?: string
|
articleImage?: string
|
||||||
|
articleSummary?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CardView: React.FC<CardViewProps> = ({
|
export const CardView: React.FC<CardViewProps> = ({
|
||||||
@@ -35,7 +36,8 @@ export const CardView: React.FC<CardViewProps> = ({
|
|||||||
eventNevent,
|
eventNevent,
|
||||||
getAuthorDisplayName,
|
getAuthorDisplayName,
|
||||||
handleReadNow,
|
handleReadNow,
|
||||||
articleImage
|
articleImage,
|
||||||
|
articleSummary
|
||||||
}) => {
|
}) => {
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [urlsExpanded, setUrlsExpanded] = useState(false)
|
const [urlsExpanded, setUrlsExpanded] = useState(false)
|
||||||
@@ -122,7 +124,11 @@ export const CardView: React.FC<CardViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{bookmark.parsedContent ? (
|
{isArticle && articleSummary ? (
|
||||||
|
<div className="bookmark-content article-summary">
|
||||||
|
<ContentWithResolvedProfiles content={articleSummary} />
|
||||||
|
</div>
|
||||||
|
) : bookmark.parsedContent ? (
|
||||||
<div className="bookmark-content">
|
<div className="bookmark-content">
|
||||||
{shouldTruncate && bookmark.content
|
{shouldTruncate && bookmark.content
|
||||||
? <ContentWithResolvedProfiles content={`${bookmark.content.slice(0, 210).trimEnd()}…`} />
|
? <ContentWithResolvedProfiles content={`${bookmark.content.slice(0, 210).trimEnd()}…`} />
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface CompactViewProps {
|
|||||||
getIconForUrlType: IconGetter
|
getIconForUrlType: IconGetter
|
||||||
firstUrlClassification: { buttonText: string } | null
|
firstUrlClassification: { buttonText: string } | null
|
||||||
articleImage?: string
|
articleImage?: string
|
||||||
|
articleSummary?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CompactView: React.FC<CompactViewProps> = ({
|
export const CompactView: React.FC<CompactViewProps> = ({
|
||||||
@@ -24,7 +25,8 @@ export const CompactView: React.FC<CompactViewProps> = ({
|
|||||||
extractedUrls,
|
extractedUrls,
|
||||||
onSelectUrl,
|
onSelectUrl,
|
||||||
getIconForUrlType,
|
getIconForUrlType,
|
||||||
firstUrlClassification
|
firstUrlClassification,
|
||||||
|
articleSummary
|
||||||
}) => {
|
}) => {
|
||||||
const isArticle = bookmark.kind === 30023
|
const isArticle = bookmark.kind === 30023
|
||||||
const isWebBookmark = bookmark.kind === 39701
|
const isWebBookmark = bookmark.kind === 39701
|
||||||
@@ -40,6 +42,11 @@ export const CompactView: React.FC<CompactViewProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For articles, prefer summary; for others, use content
|
||||||
|
const displayText = isArticle && articleSummary
|
||||||
|
? articleSummary
|
||||||
|
: bookmark.content
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${bookmark.id}-${index}`} className={`individual-bookmark compact ${bookmark.isPrivate ? 'private-bookmark' : ''}`}>
|
<div key={`${bookmark.id}-${index}`} className={`individual-bookmark compact ${bookmark.isPrivate ? 'private-bookmark' : ''}`}>
|
||||||
<div
|
<div
|
||||||
@@ -63,9 +70,9 @@ export const CompactView: React.FC<CompactViewProps> = ({
|
|||||||
<FontAwesomeIcon icon={faBookmark} className="bookmark-visibility public" />
|
<FontAwesomeIcon icon={faBookmark} className="bookmark-visibility public" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{bookmark.content && (
|
{displayText && (
|
||||||
<div className="compact-text">
|
<div className="compact-text">
|
||||||
<ContentWithResolvedProfiles content={bookmark.content.slice(0, 60) + (bookmark.content.length > 60 ? '…' : '')} />
|
<ContentWithResolvedProfiles content={displayText.slice(0, 60) + (displayText.length > 60 ? '…' : '')} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="bookmark-date-compact">{formatDate(bookmark.created_at)}</span>
|
<span className="bookmark-date-compact">{formatDate(bookmark.created_at)}</span>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface LargeViewProps {
|
|||||||
eventNevent?: string
|
eventNevent?: string
|
||||||
getAuthorDisplayName: () => string
|
getAuthorDisplayName: () => string
|
||||||
handleReadNow: (e: React.MouseEvent<HTMLButtonElement>) => void
|
handleReadNow: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
|
articleSummary?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LargeView: React.FC<LargeViewProps> = ({
|
export const LargeView: React.FC<LargeViewProps> = ({
|
||||||
@@ -32,7 +33,8 @@ export const LargeView: React.FC<LargeViewProps> = ({
|
|||||||
authorNpub,
|
authorNpub,
|
||||||
eventNevent,
|
eventNevent,
|
||||||
getAuthorDisplayName,
|
getAuthorDisplayName,
|
||||||
handleReadNow
|
handleReadNow,
|
||||||
|
articleSummary
|
||||||
}) => {
|
}) => {
|
||||||
const isArticle = bookmark.kind === 30023
|
const isArticle = bookmark.kind === 30023
|
||||||
|
|
||||||
@@ -59,7 +61,11 @@ export const LargeView: React.FC<LargeViewProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="large-content">
|
<div className="large-content">
|
||||||
{bookmark.content && (
|
{isArticle && articleSummary ? (
|
||||||
|
<div className="large-text article-summary">
|
||||||
|
<ContentWithResolvedProfiles content={articleSummary} />
|
||||||
|
</div>
|
||||||
|
) : bookmark.content && (
|
||||||
<div className="large-text">
|
<div className="large-text">
|
||||||
<ContentWithResolvedProfiles content={bookmark.content} />
|
<ContentWithResolvedProfiles content={bookmark.content} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react'
|
import React, { useState, useEffect, useMemo } from 'react'
|
||||||
import { useParams, useLocation } from 'react-router-dom'
|
import { useParams, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { Hooks } from 'applesauce-react'
|
import { Hooks } from 'applesauce-react'
|
||||||
import { useEventStore } from 'applesauce-react/hooks'
|
import { useEventStore } from 'applesauce-react/hooks'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
@@ -22,7 +22,7 @@ import { HighlightVisibility } from './HighlightsPanel'
|
|||||||
import { HighlightButton, HighlightButtonRef } from './HighlightButton'
|
import { HighlightButton, HighlightButtonRef } from './HighlightButton'
|
||||||
import { createHighlight, eventToHighlight } from '../services/highlightCreationService'
|
import { createHighlight, eventToHighlight } from '../services/highlightCreationService'
|
||||||
import { useRef, useCallback } from 'react'
|
import { useRef, useCallback } from 'react'
|
||||||
import { NostrEvent } from 'nostr-tools'
|
import { NostrEvent, nip19 } from 'nostr-tools'
|
||||||
export type ViewMode = 'compact' | 'cards' | 'large'
|
export type ViewMode = 'compact' | 'cards' | 'large'
|
||||||
|
|
||||||
interface BookmarksProps {
|
interface BookmarksProps {
|
||||||
@@ -33,6 +33,7 @@ interface BookmarksProps {
|
|||||||
const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||||
const { naddr } = useParams<{ naddr?: string }>()
|
const { naddr } = useParams<{ naddr?: string }>()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// Extract external URL from /r/* route
|
// Extract external URL from /r/* route
|
||||||
const externalUrl = location.pathname.startsWith('/r/')
|
const externalUrl = location.pathname.startsWith('/r/')
|
||||||
@@ -209,6 +210,24 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
const handleSelectUrl = async (url: string, bookmark?: BookmarkReference) => {
|
const handleSelectUrl = async (url: string, bookmark?: BookmarkReference) => {
|
||||||
if (!relayPool) return
|
if (!relayPool) return
|
||||||
|
|
||||||
|
// Update the URL path based on content type
|
||||||
|
if (bookmark && bookmark.kind === 30023) {
|
||||||
|
// For nostr articles, navigate to /a/:naddr
|
||||||
|
const dTag = bookmark.tags.find(t => t[0] === 'd')?.[1] || ''
|
||||||
|
if (dTag && bookmark.pubkey) {
|
||||||
|
const pointer = {
|
||||||
|
identifier: dTag,
|
||||||
|
kind: 30023,
|
||||||
|
pubkey: bookmark.pubkey,
|
||||||
|
}
|
||||||
|
const naddr = nip19.naddrEncode(pointer)
|
||||||
|
navigate(`/a/${naddr}`)
|
||||||
|
}
|
||||||
|
} else if (url) {
|
||||||
|
// For external URLs, navigate to /r/:url
|
||||||
|
navigate(`/r/${url}`)
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedUrl(url)
|
setSelectedUrl(url)
|
||||||
setReaderLoading(true)
|
setReaderLoading(true)
|
||||||
setReaderContent(undefined)
|
setReaderContent(undefined)
|
||||||
|
|||||||
Reference in New Issue
Block a user