mirror of
https://github.com/lumehq/lume.git
synced 2025-09-18 18:20:32 +02:00
use new note parser for note component
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ pnpm-lock.yaml
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
/.gtm/
|
||||||
|
@@ -1,21 +1,14 @@
|
|||||||
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
|
|
||||||
import NoteMetadata from '@lume/app/newsfeed/components/note/metadata';
|
import NoteMetadata from '@lume/app/newsfeed/components/note/metadata';
|
||||||
import { NoteParent } from '@lume/app/newsfeed/components/note/parent';
|
import { NoteParent } from '@lume/app/newsfeed/components/note/parent';
|
||||||
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
|
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
|
||||||
|
import { noteParser } from '@lume/app/note/components/parser';
|
||||||
|
import ImagePreview from '@lume/app/note/components/preview/image';
|
||||||
|
import VideoPreview from '@lume/app/note/components/preview/video';
|
||||||
|
|
||||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||||
|
|
||||||
export default function NoteBase({ event }: { event: any }) {
|
export default function NoteBase({ event }: { event: any }) {
|
||||||
const content = contentParser(event.content, event.tags);
|
const content = noteParser(event);
|
||||||
|
|
||||||
const parentNote = () => {
|
|
||||||
if (event.parent_id) {
|
|
||||||
if (event.parent_id !== event.event_id && !event.content.includes('#[0]')) {
|
|
||||||
return <NoteParent key={event.parent_id} id={event.parent_id} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <></>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const openNote = (e) => {
|
const openNote = (e) => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
@@ -31,11 +24,15 @@ export default function NoteBase({ event }: { event: any }) {
|
|||||||
onClick={(e) => openNote(e)}
|
onClick={(e) => openNote(e)}
|
||||||
className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 px-3 py-5 hover:bg-black/20"
|
className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 px-3 py-5 hover:bg-black/20"
|
||||||
>
|
>
|
||||||
{parentNote()}
|
{event.parent_id ? <NoteParent key={event.parent_id} id={event.parent_id} /> : <></>}
|
||||||
<div className="relative z-10 flex flex-col">
|
<div className="relative z-10 flex flex-col">
|
||||||
<NoteDefaultUser pubkey={event.pubkey} time={event.created_at} />
|
<NoteDefaultUser pubkey={event.pubkey} time={event.created_at} />
|
||||||
<div className="mt-1 pl-[52px]">
|
<div className="mt-1 pl-[52px]">
|
||||||
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">{content}</div>
|
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
||||||
|
{content.parsed}
|
||||||
|
</div>
|
||||||
|
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>}
|
||||||
|
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>}
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||||
<NoteMetadata id={event.event_id} eventPubkey={event.pubkey} />
|
<NoteMetadata id={event.event_id} eventPubkey={event.pubkey} />
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
|
|
||||||
import NoteMetadata from '@lume/app/newsfeed/components/note/metadata';
|
import NoteMetadata from '@lume/app/newsfeed/components/note/metadata';
|
||||||
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
|
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
|
||||||
|
import { noteParser } from '@lume/app/note/components/parser';
|
||||||
|
import ImagePreview from '@lume/app/note/components/preview/image';
|
||||||
|
import VideoPreview from '@lume/app/note/components/preview/video';
|
||||||
import { RelayContext } from '@lume/shared/relayProvider';
|
import { RelayContext } from '@lume/shared/relayProvider';
|
||||||
import { READONLY_RELAYS } from '@lume/stores/constants';
|
import { READONLY_RELAYS } from '@lume/stores/constants';
|
||||||
|
|
||||||
@@ -33,31 +35,36 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const content = !error && data ? noteParser(data) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative pb-5">
|
<div className="relative pb-5">
|
||||||
{error && <div>failed to load</div>}
|
{error && <div>failed to load</div>}
|
||||||
{!data ? (
|
{!data ? (
|
||||||
<div className="animated-pulse">
|
<>
|
||||||
<div className="flex items-start gap-2">
|
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
|
<div className="animated-pulse relative z-10">
|
||||||
<div className="flex w-full flex-1 items-start justify-between">
|
<div className="flex items-start gap-2">
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
<div className="h-4 w-16 rounded bg-zinc-700" />
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="h-4 w-16 rounded bg-zinc-700" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="-mt-5 pl-[52px]">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="h-16 w-full rounded bg-zinc-700" />
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<div className="h-4 w-12 rounded bg-zinc-700" />
|
||||||
|
<div className="h-4 w-12 rounded bg-zinc-700" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="-mt-5 pl-[52px]">
|
</>
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="h-16 w-full rounded bg-zinc-700" />
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<div className="h-4 w-12 rounded bg-zinc-700" />
|
|
||||||
<div className="h-4 w-12 rounded bg-zinc-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
|
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
|
||||||
@@ -65,8 +72,10 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
|||||||
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||||
<div className="mt-1 pl-[52px]">
|
<div className="mt-1 pl-[52px]">
|
||||||
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
||||||
{contentParser(data.content, data.tags)}
|
{content ? content.parsed : ''}
|
||||||
</div>
|
</div>
|
||||||
|
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>}
|
||||||
|
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>}
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||||
<NoteMetadata id={data.id} eventPubkey={data.pubkey} />
|
<NoteMetadata id={data.id} eventPubkey={data.pubkey} />
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
|
|
||||||
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
|
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
|
||||||
|
import { noteParser } from '@lume/app/note/components/parser';
|
||||||
|
import ImagePreview from '@lume/app/note/components/preview/image';
|
||||||
|
import VideoPreview from '@lume/app/note/components/preview/video';
|
||||||
import { RelayContext } from '@lume/shared/relayProvider';
|
import { RelayContext } from '@lume/shared/relayProvider';
|
||||||
import { READONLY_RELAYS } from '@lume/stores/constants';
|
import { READONLY_RELAYS } from '@lume/stores/constants';
|
||||||
|
|
||||||
@@ -42,6 +44,8 @@ export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const content = !error && data ? noteParser(data) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={(e) => openNote(e)}
|
onClick={(e) => openNote(e)}
|
||||||
@@ -55,8 +59,10 @@ export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
|
|||||||
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||||
<div className="mt-1 pl-[52px]">
|
<div className="mt-1 pl-[52px]">
|
||||||
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
||||||
{contentParser(data.content, data.tags)}
|
{content ? content.parsed : ''}
|
||||||
</div>
|
</div>
|
||||||
|
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>}
|
||||||
|
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
|
|
||||||
import NoteMetadata from '@lume/app/newsfeed/components/note/metadata';
|
import NoteMetadata from '@lume/app/newsfeed/components/note/metadata';
|
||||||
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
|
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
|
||||||
|
import { noteParser } from '@lume/app/note/components/parser';
|
||||||
|
import ImagePreview from '@lume/app/note/components/preview/image';
|
||||||
|
import VideoPreview from '@lume/app/note/components/preview/video';
|
||||||
import { RelayContext } from '@lume/shared/relayProvider';
|
import { RelayContext } from '@lume/shared/relayProvider';
|
||||||
import { READONLY_RELAYS } from '@lume/stores/constants';
|
import { READONLY_RELAYS } from '@lume/stores/constants';
|
||||||
|
|
||||||
@@ -44,14 +46,28 @@ export const RootNote = memo(function RootNote({ id, fallback }: { id: string; f
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const content = !error && data ? noteParser(parseFallback) : null;
|
||||||
|
|
||||||
if (parseFallback) {
|
if (parseFallback) {
|
||||||
|
const contentFallback = noteParser(parseFallback);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={(e) => openNote(e)} className="relative z-10 flex flex-col">
|
<div onClick={(e) => openNote(e)} className="relative z-10 flex flex-col">
|
||||||
<NoteDefaultUser pubkey={parseFallback.pubkey} time={parseFallback.created_at} />
|
<NoteDefaultUser pubkey={parseFallback.pubkey} time={parseFallback.created_at} />
|
||||||
<div className="mt-1 pl-[52px]">
|
<div className="mt-1 pl-[52px]">
|
||||||
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
||||||
{contentParser(parseFallback.content, parseFallback.tags)}
|
{contentFallback.parsed}
|
||||||
</div>
|
</div>
|
||||||
|
{Array.isArray(contentFallback.images) && contentFallback.images.length ? (
|
||||||
|
<ImagePreview urls={contentFallback.images} />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
{Array.isArray(contentFallback.videos) && contentFallback.videos.length ? (
|
||||||
|
<VideoPreview urls={contentFallback.videos} />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||||
<NoteMetadata id={parseFallback.id} eventPubkey={parseFallback.pubkey} />
|
<NoteMetadata id={parseFallback.id} eventPubkey={parseFallback.pubkey} />
|
||||||
@@ -90,8 +106,10 @@ export const RootNote = memo(function RootNote({ id, fallback }: { id: string; f
|
|||||||
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||||
<div className="mt-1 pl-[52px]">
|
<div className="mt-1 pl-[52px]">
|
||||||
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
||||||
{contentParser(data.content, data.tags)}
|
{content ? content.parsed : ''}
|
||||||
</div>
|
</div>
|
||||||
|
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>}
|
||||||
|
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>}
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||||
<NoteMetadata id={data.id} eventPubkey={data.pubkey} />
|
<NoteMetadata id={data.id} eventPubkey={data.pubkey} />
|
||||||
|
@@ -11,10 +11,10 @@ export const NoteDefaultUser = ({ pubkey, time }: { pubkey: string; time: number
|
|||||||
const { user, isError, isLoading } = useProfile(pubkey);
|
const { user, isError, isLoading } = useProfile(pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex h-11 items-center gap-2">
|
<div className="group relative z-10 flex h-11 items-center gap-2">
|
||||||
{isError || isLoading ? (
|
{isError || isLoading ? (
|
||||||
<>
|
<>
|
||||||
<div className="relative h-11 w-11 shrink animate-pulse overflow-hidden rounded-md bg-white bg-zinc-800"></div>
|
<div className="h-11 w-11 shrink animate-pulse overflow-hidden rounded-md bg-white bg-zinc-800"></div>
|
||||||
<div className="flex w-full flex-1 items-start justify-between">
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
@@ -26,7 +26,7 @@ export const NoteDefaultUser = ({ pubkey, time }: { pubkey: string; time: number
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
<div className="h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||||
<img
|
<img
|
||||||
src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`}
|
src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
|
@@ -9,9 +9,12 @@ export const noteParser = (event: Event) => {
|
|||||||
const references = parseReferences(event);
|
const references = parseReferences(event);
|
||||||
const content = { original: event.content, parsed: event.content, images: [], videos: [], others: [] };
|
const content = { original: event.content, parsed: event.content, images: [], videos: [], others: [] };
|
||||||
|
|
||||||
|
// remove extra whitespace
|
||||||
|
content.parsed = content.parsed.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
// handle media
|
// handle media
|
||||||
content.original.match(getURLs)?.forEach((url) => {
|
content.original.match(getURLs)?.forEach((url) => {
|
||||||
if (url.match(/\.(jpg|jpeg|gif|png|webp|avif)$/i)) {
|
if (url.match(/\.(jpg|jpeg|gif|png|webp|avif)$/im)) {
|
||||||
// image url
|
// image url
|
||||||
content.images.push(url.trim());
|
content.images.push(url.trim());
|
||||||
// remove url from original content
|
// remove url from original content
|
||||||
@@ -21,7 +24,7 @@ export const noteParser = (event: Event) => {
|
|||||||
content.videos.push(url.trim());
|
content.videos.push(url.trim());
|
||||||
// remove url from original content
|
// remove url from original content
|
||||||
content.parsed = content.parsed.replace(url, '');
|
content.parsed = content.parsed.replace(url, '');
|
||||||
} else if (url.match(/\.(mp4|webm|mov)$/i)) {
|
} else if (url.match(/\.(mp4|webm|mov)$/im)) {
|
||||||
// video
|
// video
|
||||||
content.videos.push(url.trim());
|
content.videos.push(url.trim());
|
||||||
// remove url from original content
|
// remove url from original content
|
||||||
@@ -34,13 +37,19 @@ export const noteParser = (event: Event) => {
|
|||||||
// handle note references
|
// handle note references
|
||||||
references?.forEach((reference) => {
|
references?.forEach((reference) => {
|
||||||
if (reference?.profile) {
|
if (reference?.profile) {
|
||||||
content.parsed.replace(reference.text, `<NoteMentionUser pubkey="${reference.profile.pubkey}" />`);
|
content.parsed = content.parsed.replace(
|
||||||
|
reference.text,
|
||||||
|
`<NoteMentionUser pubkey="${reference.profile.pubkey}" />`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (reference?.event) {
|
if (reference?.event) {
|
||||||
content.parsed.replace(reference.text, `<NoteQuote id="${reference.event.id}" />;`);
|
content.parsed = content.parsed.replace(reference.text, `<NoteQuote id="${reference.event.id}" />;`);
|
||||||
}
|
}
|
||||||
if (reference?.address) {
|
if (reference?.address) {
|
||||||
content.parsed.replace(reference.text, `<a href="${reference.address}" target="_blank" />`);
|
content.parsed = content.parsed.replace(
|
||||||
|
reference.text,
|
||||||
|
`<a href="${reference.address}" target="_blank">${reference.address}</>`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
31
src/app/note/components/preview/image.tsx
Normal file
31
src/app/note/components/preview/image.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export default function ImagePreview({ urls }: { urls: string[] }) {
|
||||||
|
return (
|
||||||
|
<div className="mt-2 grid h-full w-full grid-cols-2">
|
||||||
|
{urls.length === 1 ? (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<img
|
||||||
|
src={urls[0]}
|
||||||
|
alt="image"
|
||||||
|
className="h-auto w-full rounded-lg object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
style={{ contentVisibility: 'auto' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
urls.map((url: string) => (
|
||||||
|
<div key={url} className="col-span-1">
|
||||||
|
<img
|
||||||
|
src={url}
|
||||||
|
alt="image"
|
||||||
|
className="h-auto w-full rounded-lg object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
style={{ contentVisibility: 'auto' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
11
src/app/note/components/preview/video.tsx
Normal file
11
src/app/note/components/preview/video.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { MediaOutlet, MediaPlayer } from '@vidstack/react';
|
||||||
|
|
||||||
|
export default function VideoPreview({ urls }: { urls: string[] }) {
|
||||||
|
return (
|
||||||
|
<div onClick={(e) => e.stopPropagation()} className="relative mt-2 flex w-full flex-col rounded-lg bg-zinc-950">
|
||||||
|
<MediaPlayer src={urls[0]} poster="" controls>
|
||||||
|
<MediaOutlet />
|
||||||
|
</MediaPlayer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user