Lazy load TextPreview (#281)

* feat: add lazy loading for TextPreview

* styling and truncate
This commit is contained in:
Anton Tranelis 2025-07-04 16:16:19 +02:00 committed by GitHub
parent 28dd16dfeb
commit 29fadc6ef0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 52 additions and 67 deletions

View File

@ -0,0 +1,33 @@
import { useEffect, useRef, useState } from 'react'
import { TextPreview } from './TextPreview'
import type { Item } from '#types/Item'
export const LazyTextPreview = ({ item }: { item: Item }) => {
const [visible, setVisible] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setVisible(true)
observer.disconnect()
}
})
if (ref.current) observer.observe(ref.current)
return () => observer.disconnect()
}, [])
return (
<div ref={ref}>
{visible ? (
<TextPreview item={item} />
) : (
<div className='tw:flex tw:justify-center '>
<div className='tw:loading tw:spinner tw:h-8 tw:opacity-50' />
</div>
)}
</div>
)
}

View File

@ -1,75 +1,23 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import truncate from 'markdown-truncate'
import Markdown from 'react-markdown'
import rehypeRaw from 'rehype-raw'
import remarkBreaks from 'remark-breaks'
import remarkGfm from 'remark-gfm'
import { useAddFilterTag } from '#components/Map/hooks/useFilter'
import { useGetItemTags, useTags } from '#components/Map/hooks/useTags'
import { decodeTag } from '#utils/FormatTags'
import { RichTextEditor } from '#components/Input/RichTextEditor/RichTextEditor'
import { fixUrls, mailRegex } from '#utils/ReplaceURLs'
import type { Item } from '#types/Item'
export const TextPreview = ({ item }: { item: Item }) => {
const getItemTags = useGetItemTags()
if (!item.text) return null
// Text auf ~100 Zeichen stutzen (inkl. Ellipse „…“)
const previewRaw = truncate(item.text, { limit: 100, ellipsis: true }) as string
else {
let replacedText = truncate(item.text, { limit: 100, ellipsis: true }) as string
const withExtraHashes = previewRaw.replace(
/^(#{1,6})\s/gm,
(_match: string, hashes: string): string => `${hashes}## `,
)
replacedText = fixUrls(item.text)
return (
<div className='markdown'>
<Markdown remarkPlugins={[remarkBreaks, remarkGfm]} rehypePlugins={[rehypeRaw]}>
{removeMentionSpans(removeHashtags(withExtraHashes))}
</Markdown>
{getItemTags(item).map((tag) => (
<HashTag tag={tag} key={tag} />
))}
</div>
)
if (replacedText) {
replacedText = replacedText.replace(mailRegex, (url) => {
return `[${url}](mailto:${url})`
})
}
return <RichTextEditor defaultValue={replacedText} readOnly={true} />
}
}
export const HashTag = ({ tag }: { tag: Tag }) => {
const tags = useTags()
const t = tags.find((t) => t.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase())
const addFilterTag = useAddFilterTag()
if (!t) return null
return (
<a
className='hashtag'
style={{ color: t.color }}
key={`${t.name}`}
onClick={(e) => {
e.stopPropagation()
addFilterTag(t)
}}
>
{`#${decodeTag(tag.name)} `}
</a>
)
}
function removeMentionSpans(html) {
return html.replace(
/<span\b(?=[^>]*\bdata-type="mention")(?=[^>]*\bclass="mention")[^>]*>[\s\S]*?<\/span>/gi,
'',
)
}
function removeHashtags(str) {
return str
// 1. Hashtags entfernen, außer sie stehen am Zeilenanfang als Markdown-Heading
.replace(
/(^|\s)(?!#{1,6}\s)(#[A-Za-z0-9_]+)\b/g,
'$1'
)
// 3. Anfangs/Ende trimmen
.trim()
}

View File

@ -6,3 +6,4 @@ export { TextView } from './TextView'
export { StartEndView } from './StartEndView'
export { PopupButton } from './PopupButton'
export { TextPreview } from './TextPreview'
export { LazyTextPreview } from './LazyTextPreview'

View File

@ -3,7 +3,10 @@ import { useNavigate } from 'react-router-dom'
import { useSetSelectPosition } from '#components/Map/hooks/useSelectPosition'
import useWindowDimensions from '#components/Map/hooks/useWindowDimension'
import { StartEndView, TextPreview } from '#components/Map/Subcomponents/ItemPopupComponents'
import {
StartEndView,
LazyTextPreview,
} from '#components/Map/Subcomponents/ItemPopupComponents'
import { HeaderView } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView'
import { DateUserInfo } from './DateUserInfo'
@ -51,7 +54,7 @@ export const ItemCard = ({
></HeaderView>
<div className='tw:overflow-y-auto tw:overflow-x-hidden tw:max-h-64 fade'>
{i.layer?.itemType.show_start_end && <StartEndView item={i}></StartEndView>}
{i.layer?.itemType.show_text && <TextPreview item={i} />}
{i.layer?.itemType.show_text && <LazyTextPreview item={i} />}
</div>
<DateUserInfo item={i}></DateUserInfo>
</div>