mirror of
https://github.com/dergigi/boris.git
synced 2026-01-08 09:24:42 +01:00
Add comment explaining that we extract the image tag directly from bookmark.tags since we don't have the full NostrEvent here. When we do have full events (like in articleService), we use getArticleImage() helper from applesauce-core as intended.
123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
import React, { useState } from 'react'
|
|
import { faBookOpen, faPlay, faEye } from '@fortawesome/free-solid-svg-icons'
|
|
import { useEventModel } from 'applesauce-react/hooks'
|
|
import { Models } from 'applesauce-core'
|
|
import { npubEncode, neventEncode } from 'nostr-tools/nip19'
|
|
import { IndividualBookmark } from '../types/bookmarks'
|
|
import { extractUrlsFromContent } from '../services/bookmarkHelpers'
|
|
import { classifyUrl } from '../utils/helpers'
|
|
import { ViewMode } from './Bookmarks'
|
|
import { getPreviewImage, fetchOgImage } from '../utils/imagePreview'
|
|
import { CompactView } from './BookmarkViews/CompactView'
|
|
import { LargeView } from './BookmarkViews/LargeView'
|
|
import { CardView } from './BookmarkViews/CardView'
|
|
|
|
interface BookmarkItemProps {
|
|
bookmark: IndividualBookmark
|
|
index: number
|
|
onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => void
|
|
viewMode?: ViewMode
|
|
}
|
|
|
|
export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onSelectUrl, viewMode = 'cards' }) => {
|
|
const [ogImage, setOgImage] = useState<string | null>(null)
|
|
|
|
const short = (v: string) => `${v.slice(0, 8)}...${v.slice(-8)}`
|
|
|
|
// Extract URLs from bookmark content
|
|
const extractedUrls = extractUrlsFromContent(bookmark.content)
|
|
const hasUrls = extractedUrls.length > 0
|
|
const firstUrl = hasUrls ? extractedUrls[0] : null
|
|
const firstUrlClassification = firstUrl ? classifyUrl(firstUrl) : null
|
|
|
|
// For kind:30023 articles, extract image tag (per NIP-23)
|
|
// 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)
|
|
const isArticle = bookmark.kind === 30023
|
|
const articleImage = isArticle ? bookmark.tags.find(t => t[0] === 'image')?.[1] : undefined
|
|
|
|
// Fetch OG image for large view (hook must be at top level)
|
|
const instantPreview = firstUrl ? getPreviewImage(firstUrl, firstUrlClassification?.type || '') : null
|
|
React.useEffect(() => {
|
|
if (viewMode === 'large' && firstUrl && !instantPreview && !ogImage && !articleImage) {
|
|
fetchOgImage(firstUrl).then(setOgImage)
|
|
}
|
|
}, [viewMode, firstUrl, instantPreview, ogImage, articleImage])
|
|
|
|
// Resolve author profile using applesauce
|
|
const authorProfile = useEventModel(Models.ProfileModel, [bookmark.pubkey])
|
|
const authorNpub = npubEncode(bookmark.pubkey)
|
|
const isHexId = /^[0-9a-f]{64}$/i.test(bookmark.id)
|
|
const eventNevent = isHexId ? neventEncode({ id: bookmark.id }) : undefined
|
|
|
|
// Get display name for author
|
|
const getAuthorDisplayName = () => {
|
|
if (authorProfile?.name) return authorProfile.name
|
|
if (authorProfile?.display_name) return authorProfile.display_name
|
|
if (authorProfile?.nip05) return authorProfile.nip05
|
|
return short(bookmark.pubkey) // fallback to short pubkey
|
|
}
|
|
|
|
// use helper from kindIcon.ts
|
|
|
|
const getIconForUrlType = (url: string) => {
|
|
const classification = classifyUrl(url)
|
|
switch (classification.type) {
|
|
case 'youtube':
|
|
case 'video':
|
|
return faPlay
|
|
case 'image':
|
|
return faEye
|
|
default:
|
|
return faBookOpen
|
|
}
|
|
}
|
|
|
|
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, pubkey: bookmark.pubkey })
|
|
}
|
|
return
|
|
}
|
|
|
|
// For regular bookmarks with URLs
|
|
if (!hasUrls) return
|
|
const firstUrl = extractedUrls[0]
|
|
if (onSelectUrl) {
|
|
onSelectUrl(firstUrl)
|
|
} else {
|
|
window.open(firstUrl, '_blank')
|
|
}
|
|
}
|
|
|
|
const sharedProps = {
|
|
bookmark,
|
|
index,
|
|
hasUrls,
|
|
extractedUrls,
|
|
onSelectUrl,
|
|
getIconForUrlType,
|
|
firstUrlClassification,
|
|
authorNpub,
|
|
eventNevent,
|
|
getAuthorDisplayName,
|
|
handleReadNow,
|
|
articleImage
|
|
}
|
|
|
|
if (viewMode === 'compact') {
|
|
return <CompactView {...sharedProps} />
|
|
}
|
|
|
|
if (viewMode === 'large') {
|
|
const previewImage = articleImage || instantPreview || ogImage
|
|
return <LargeView {...sharedProps} previewImage={previewImage} />
|
|
}
|
|
|
|
return <CardView {...sharedProps} articleImage={articleImage} />
|
|
}
|