Browse Source

more bug-fixes

imwald
Silberengel 5 months ago
parent
commit
58ce59f2bf
  1. 3
      src/components/Content/index.tsx
  2. 24
      src/components/ImageGallery/index.tsx
  3. 6
      src/components/Note/LongFormArticle/index.tsx
  4. 4
      src/components/Note/LongFormArticlePreview.tsx
  5. 155
      src/components/UniversalContent/SimpleContent.tsx
  6. 4
      src/components/WebPreview/index.tsx
  7. 4
      src/pages/secondary/NotePage/index.tsx

3
src/components/Content/index.tsx

@ -9,6 +9,7 @@ import {
EmbeddedWebsocketUrlParser, EmbeddedWebsocketUrlParser,
parseContent parseContent
} from '@/lib/content-parser' } from '@/lib/content-parser'
import logger from '@/lib/logger'
import { getImetaInfosFromEvent } from '@/lib/event' import { getImetaInfosFromEvent } from '@/lib/event'
import { getEmojiInfosFromEmojiTags, getImetaInfoFromImetaTag } from '@/lib/tag' import { getEmojiInfosFromEmojiTags, getImetaInfoFromImetaTag } from '@/lib/tag'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -119,6 +120,7 @@ export default function Content({
} }
let imageIndex = 0 let imageIndex = 0
logger.debug('[Content] Parsed content:', { nodeCount: nodes.length, allImages: allImages.length, nodes: nodes.map(n => ({ type: n.type, data: Array.isArray(n.data) ? n.data.length : n.data })) })
return ( return (
<div className={cn('text-wrap break-words whitespace-pre-wrap', className)}> <div className={cn('text-wrap break-words whitespace-pre-wrap', className)}>
{nodes.map((node, index) => { {nodes.map((node, index) => {
@ -129,6 +131,7 @@ export default function Content({
const start = imageIndex const start = imageIndex
const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1) const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1)
imageIndex = end imageIndex = end
logger.debug('[Content] Creating ImageGallery:', { nodeType: node.type, start, end, totalImages: allImages.length, nodeData: Array.isArray(node.data) ? node.data.length : node.data })
return ( return (
<ImageGallery <ImageGallery
className="mt-2" className="mt-2"

24
src/components/ImageGallery/index.tsx

@ -1,5 +1,6 @@
import { randomString } from '@/lib/random' import { randomString } from '@/lib/random'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import logger from '@/lib/logger'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import modalManager from '@/services/modal-manager.service' import modalManager from '@/services/modal-manager.service'
import { TImetaInfo } from '@/types' import { TImetaInfo } from '@/types'
@ -39,7 +40,9 @@ export default function ImageGallery({
const handlePhotoClick = (event: React.MouseEvent, current: number) => { const handlePhotoClick = (event: React.MouseEvent, current: number) => {
event.stopPropagation() event.stopPropagation()
event.preventDefault() event.preventDefault()
setIndex(start + current) const newIndex = start + current
logger.debug('[ImageGallery] Click:', { start, current, newIndex, totalImages: images.length, displayImages: displayImages.length })
setIndex(newIndex)
} }
const displayImages = images.slice(start, end) const displayImages = images.slice(start, end)
@ -62,7 +65,7 @@ export default function ImageGallery({
imageContent = ( imageContent = (
<Image <Image
key={0} key={0}
className="max-h-[80vh] sm:max-h-[50vh] cursor-zoom-in object-contain" className="max-h-[80vh] sm:max-h-[50vh] cursor-zoom-in object-contain max-w-[400px]"
classNames={{ classNames={{
errorPlaceholder: 'aspect-square h-[30vh]' errorPlaceholder: 'aspect-square h-[30vh]'
}} }}
@ -72,7 +75,7 @@ export default function ImageGallery({
) )
} else if (displayImages.length === 2 || displayImages.length === 4) { } else if (displayImages.length === 2 || displayImages.length === 4) {
imageContent = ( imageContent = (
<div className="grid grid-cols-2 gap-2 w-full"> <div className="grid grid-cols-2 gap-2 w-full max-w-[400px]">
{displayImages.map((image, i) => ( {displayImages.map((image, i) => (
<Image <Image
key={i} key={i}
@ -85,7 +88,7 @@ export default function ImageGallery({
) )
} else { } else {
imageContent = ( imageContent = (
<div className="grid grid-cols-3 gap-2 w-full"> <div className="grid grid-cols-3 gap-2 w-full max-w-[400px]">
{displayImages.map((image, i) => ( {displayImages.map((image, i) => (
<Image <Image
key={i} key={i}
@ -99,17 +102,21 @@ export default function ImageGallery({
} }
return ( return (
<div className={cn(displayImages.length === 1 ? 'w-fit max-w-full' : 'w-full', className)}> <div className={cn(displayImages.length === 1 ? 'w-fit max-w-[400px]' : 'w-full', className)}>
{imageContent} {imageContent}
{index >= 0 && {index >= 0 &&
createPortal( createPortal(
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<Lightbox <Lightbox
index={index} index={index}
slides={images.map(({ url, alt }) => ({ slides={(() => {
const slides = images.map(({ url, alt }) => ({
src: url, src: url,
alt: alt || url alt: alt || url
}))} }))
logger.debug('[ImageGallery] Lightbox slides:', { index, slidesCount: slides.length, slides })
return slides
})()}
plugins={[Zoom]} plugins={[Zoom]}
open={index >= 0} open={index >= 0}
close={() => setIndex(-1)} close={() => setIndex(-1)}
@ -121,6 +128,9 @@ export default function ImageGallery({
styles={{ styles={{
toolbar: { paddingTop: '2.25rem' } toolbar: { paddingTop: '2.25rem' }
}} }}
carousel={{
finite: false
}}
/> />
</div>, </div>,
document.body document.body

6
src/components/Note/LongFormArticle/index.tsx

@ -78,9 +78,9 @@ export default function LongFormArticle({
img: (props) => ( img: (props) => (
<ImageWithLightbox <ImageWithLightbox
image={{ url: props.src || '', pubkey: event.pubkey }} image={{ url: props.src || '', pubkey: event.pubkey }}
className="max-h-[80vh] sm:max-h-[50vh] object-contain my-0" className="max-h-[80vh] sm:max-h-[50vh] object-contain my-0 max-w-[400px]"
classNames={{ classNames={{
wrapper: 'w-fit max-w-full' wrapper: 'w-fit max-w-[400px]'
}} }}
/> />
) )
@ -101,7 +101,7 @@ export default function LongFormArticle({
{metadata.image && ( {metadata.image && (
<ImageWithLightbox <ImageWithLightbox
image={{ url: metadata.image, pubkey: event.pubkey }} image={{ url: metadata.image, pubkey: event.pubkey }}
className="w-full aspect-[3/1] object-cover my-0" className="w-full max-w-[400px] aspect-[3/1] object-cover my-0"
/> />
)} )}
<Markdown <Markdown

4
src/components/Note/LongFormArticlePreview.tsx

@ -48,7 +48,7 @@ export default function LongFormArticlePreview({
{metadata.image && autoLoadMedia && ( {metadata.image && autoLoadMedia && (
<Image <Image
image={{ url: metadata.image, pubkey: event.pubkey }} image={{ url: metadata.image, pubkey: event.pubkey }}
className="w-full aspect-video" className="w-full max-w-[400px] aspect-video"
hideIfError hideIfError
/> />
)} )}
@ -67,7 +67,7 @@ export default function LongFormArticlePreview({
{metadata.image && autoLoadMedia && ( {metadata.image && autoLoadMedia && (
<Image <Image
image={{ url: metadata.image, pubkey: event.pubkey }} image={{ url: metadata.image, pubkey: event.pubkey }}
className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44" className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44 max-w-[400px]"
hideIfError hideIfError
/> />
)} )}

155
src/components/UniversalContent/SimpleContent.tsx

@ -1,13 +1,14 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { cleanUrl } from '@/lib/url' import { cleanUrl } from '@/lib/url'
import { getImetaInfosFromEvent } from '@/lib/event' import { getImetaInfosFromEvent } from '@/lib/event'
import logger from '@/lib/logger'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import Markdown from 'react-markdown' import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import { remarkNostr } from '../Note/LongFormArticle/remarkNostr' import { remarkNostr } from '../Note/LongFormArticle/remarkNostr'
import NostrNode from '../Note/LongFormArticle/NostrNode' import NostrNode from '../Note/LongFormArticle/NostrNode'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import ImageWithLightbox from '../ImageWithLightbox' import ImageGallery from '../ImageGallery'
import MediaPlayer from '../MediaPlayer' import MediaPlayer from '../MediaPlayer'
interface SimpleContentProps { interface SimpleContentProps {
@ -75,90 +76,108 @@ export default function SimpleContent({
const markdownLines: string[] = [] const markdownLines: string[] = []
let key = 0 let key = 0
// Extract all image URLs from content
const imageUrls: string[] = []
lines.forEach((line) => { lines.forEach((line) => {
// Check if line contains an image URL
const imageMatch = line.match(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|heic|svg))/i) const imageMatch = line.match(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|heic|svg))/i)
if (imageMatch) { if (imageMatch) {
const imageUrl = imageMatch[1] imageUrls.push(imageMatch[1])
const imageInfo = imetaInfos.find((info) => info.url === imageUrl) }
const imageData = imageInfo || { url: imageUrl, pubkey: event?.pubkey } })
elements.push( // Extract all video URLs from content
<div key={key++} className="my-4"> const videoUrls: string[] = []
<ImageWithLightbox lines.forEach((line) => {
image={imageData} const videoMatch = line.match(/(https?:\/\/[^\s]+\.(mp4|webm|ogg|avi|mov|wmv|flv|mkv|m4v))/i)
className="max-w-full h-auto rounded-lg cursor-zoom-in" if (videoMatch) {
/> videoUrls.push(videoMatch[1])
</div> }
) })
// Add the rest of the line as text if there's anything else // Get all unique images - prioritize imeta tags, then add content images that aren't in imeta
const beforeImage = line.substring(0, imageMatch.index).trim() const allImageInfos = [...imetaInfos] // Start with imeta images
const afterImage = line.substring(imageMatch.index! + imageUrl.length).trim() const processedUrls = new Set(imetaInfos.map(info => info.url))
if (beforeImage || afterImage) { // Add content images that aren't already in imeta
markdownLines.push(beforeImage + afterImage) imageUrls.forEach(url => {
if (!processedUrls.has(url)) {
allImageInfos.push({ url: url, pubkey: event?.pubkey })
processedUrls.add(url)
} }
} else { })
// Check if line contains a video URL
const videoMatch = line.match(/(https?:\/\/[^\s]+\.(mp4|webm|ogg|avi|mov|wmv|flv|mkv|m4v))/i)
if (videoMatch) { // Get all unique videos - prioritize imeta tags, then add content videos that aren't in imeta
const originalVideoUrl = videoMatch[1] const allVideoInfos = imetaInfos.filter(info => {
// Clean the video URL to remove tracking parameters // Check if the imeta info is a video by looking at the URL extension
const cleanedVideoUrl = (() => { const url = info.url
const extension = url.split('.').pop()?.toLowerCase()
return extension && ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v'].includes(extension)
})
const processedVideoUrls = new Set(allVideoInfos.map(info => {
try {
return new URL(cleanUrl(info.url)).href
} catch {
return cleanUrl(info.url)
}
}))
// Add content videos that aren't already in imeta
videoUrls.forEach(url => {
const cleanedUrl = (() => {
try { try {
return cleanUrl(originalVideoUrl) return cleanUrl(url)
} catch { } catch {
return originalVideoUrl return url
} }
})() })()
// Check if this video URL is already handled by imeta tags const normalizedUrl = (() => {
const normalizedVideoUrl = (() => {
try { try {
return new URL(cleanedVideoUrl).href return new URL(cleanedUrl).href
} catch { } catch {
return cleanedVideoUrl return cleanedUrl
} }
})() })()
if (!imetaVideoUrls.includes(normalizedVideoUrl)) { if (!processedVideoUrls.has(normalizedUrl)) {
allVideoInfos.push({ url: url, pubkey: event?.pubkey })
processedVideoUrls.add(normalizedUrl)
}
})
logger.debug('[SimpleContent] Processing content:', {
totalLines: lines.length,
imetaImages: imetaInfos.length,
contentImages: imageUrls.length,
totalUniqueImages: allImageInfos.length,
imetaVideos: imetaInfos.filter(info => {
const extension = info.url.split('.').pop()?.toLowerCase()
return extension && ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v'].includes(extension)
}).length,
contentVideos: videoUrls.length,
totalUniqueVideos: allVideoInfos.length
})
// If we have images, create a single ImageGallery for all of them
if (allImageInfos.length > 0) {
logger.debug('[SimpleContent] Creating ImageGallery with all unique images:', {
count: allImageInfos.length,
urls: allImageInfos.map(i => i.url)
})
elements.push( elements.push(
<div key={key++} className="my-4"> <div key={key++} className="my-4">
<MediaPlayer <ImageGallery
src={cleanedVideoUrl} images={allImageInfos}
className="max-w-full h-auto rounded-lg" className="max-w-[400px]"
/> />
</div> </div>
) )
} }
// Add the rest of the line as text if there's anything else // Add all unique videos to elements
const beforeVideo = line.substring(0, videoMatch.index).trim() allVideoInfos.forEach(videoInfo => {
const afterVideo = line.substring(videoMatch.index! + originalVideoUrl.length).trim()
if (beforeVideo || afterVideo) {
markdownLines.push(beforeVideo + afterVideo)
}
} else {
// Regular text line - add to markdown processing
markdownLines.push(line)
}
}
})
// Add imeta videos to the elements
imetaInfos
.filter(info => {
// Check if the imeta info is a video by looking at the URL extension
const url = info.url
const extension = url.split('.').pop()?.toLowerCase()
return extension && ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v'].includes(extension)
})
.forEach(videoInfo => {
// Clean the imeta video URL to remove tracking parameters
const cleanedVideoUrl = (() => { const cleanedVideoUrl = (() => {
try { try {
return cleanUrl(videoInfo.url) return cleanUrl(videoInfo.url)
@ -171,12 +190,26 @@ export default function SimpleContent({
<div key={key++} className="my-4"> <div key={key++} className="my-4">
<MediaPlayer <MediaPlayer
src={cleanedVideoUrl} src={cleanedVideoUrl}
className="max-w-full h-auto rounded-lg" className="max-w-[400px] h-auto rounded-lg"
/> />
</div> </div>
) )
}) })
// Process lines for text content (excluding images and videos)
lines.forEach((line) => {
// Skip lines that contain images or videos (already processed above)
const hasImage = line.match(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|heic|svg))/i)
const hasVideo = line.match(/(https?:\/\/[^\s]+\.(mp4|webm|ogg|avi|mov|wmv|flv|mkv|m4v))/i)
if (hasImage || hasVideo) {
return // Skip this line as it's already processed
}
// Regular text line - add to markdown processing
markdownLines.push(line)
})
return { return {
markdownContent: markdownLines.join('\n'), markdownContent: markdownLines.join('\n'),
mediaElements: elements mediaElements: elements

4
src/components/WebPreview/index.tsx

@ -35,7 +35,7 @@ export default function WebPreview({ url, className }: { url: string; className?
window.open(url, '_blank') window.open(url, '_blank')
}} }}
> >
<Image image={{ url: image }} className="w-full h-44 rounded-none" hideIfError /> <Image image={{ url: image }} className="w-full max-w-[400px] h-44 rounded-none" hideIfError />
<div className="bg-muted p-2 w-full"> <div className="bg-muted p-2 w-full">
<div className="text-xs text-muted-foreground">{hostname}</div> <div className="text-xs text-muted-foreground">{hostname}</div>
<div className="font-semibold line-clamp-1">{title}</div> <div className="font-semibold line-clamp-1">{title}</div>
@ -55,7 +55,7 @@ export default function WebPreview({ url, className }: { url: string; className?
{image && ( {image && (
<Image <Image
image={{ url: image }} image={{ url: image }}
className="aspect-[4/3] xl:aspect-video bg-foreground h-44 rounded-none" className="aspect-[4/3] xl:aspect-video bg-foreground h-44 max-w-[400px] rounded-none"
hideIfError hideIfError
/> />
)} )}

4
src/pages/secondary/NotePage/index.tsx

@ -109,7 +109,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string;
return ( return (
<SecondaryPageLayout ref={ref} index={index} title={hideTitlebar ? undefined : getNoteTypeTitle(finalEvent.kind)} displayScrollToTopButton> <SecondaryPageLayout ref={ref} index={index} title={hideTitlebar ? undefined : getNoteTypeTitle(finalEvent.kind)} displayScrollToTopButton>
<div className="px-4 pt-3"> <div className="px-4 pt-3 max-w-4xl mx-auto">
{rootITag && <ExternalRoot value={rootITag[1]} />} {rootITag && <ExternalRoot value={rootITag[1]} />}
{rootEventId && rootEventId !== parentEventId && ( {rootEventId && rootEventId !== parentEventId && (
<ParentNote <ParentNote
@ -139,7 +139,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string;
<NoteStats className="mt-3" event={finalEvent} fetchIfNotExisting displayTopZapsAndLikes /> <NoteStats className="mt-3" event={finalEvent} fetchIfNotExisting displayTopZapsAndLikes />
</div> </div>
<Separator className="mt-4" /> <Separator className="mt-4" />
<div className="px-4 pb-4"> <div className="px-4 pb-4 max-w-4xl mx-auto">
<NoteInteractions key={`note-interactions-${finalEvent.id}`} pageIndex={index} event={finalEvent} /> <NoteInteractions key={`note-interactions-${finalEvent.id}`} pageIndex={index} event={finalEvent} />
</div> </div>
</SecondaryPageLayout> </SecondaryPageLayout>

Loading…
Cancel
Save