mirror of
https://github.com/lumehq/lume.git
synced 2025-03-28 18:52:33 +01:00
new parser
This commit is contained in:
parent
912c740c51
commit
dad388c6ab
@ -59,6 +59,7 @@
|
|||||||
"@tiptap/react": "^2.1.12",
|
"@tiptap/react": "^2.1.12",
|
||||||
"@tiptap/starter-kit": "^2.1.12",
|
"@tiptap/starter-kit": "^2.1.12",
|
||||||
"@tiptap/suggestion": "^2.1.12",
|
"@tiptap/suggestion": "^2.1.12",
|
||||||
|
"cobe": "^0.6.3",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"destr": "^2.0.2",
|
"destr": "^2.0.2",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
@ -79,7 +80,9 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.47.0",
|
"react-hook-form": "^7.47.0",
|
||||||
"react-hotkeys-hook": "^4.4.1",
|
"react-hotkeys-hook": "^4.4.1",
|
||||||
|
"react-medium-image-zoom": "^5.1.8",
|
||||||
"react-router-dom": "^6.18.0",
|
"react-router-dom": "^6.18.0",
|
||||||
|
"react-string-replace": "^1.1.1",
|
||||||
"reactflow": "^11.9.4",
|
"reactflow": "^11.9.4",
|
||||||
"sonner": "^1.1.0",
|
"sonner": "^1.1.0",
|
||||||
"tailwind-scrollbar": "^3.0.5",
|
"tailwind-scrollbar": "^3.0.5",
|
||||||
|
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@ -128,6 +128,9 @@ dependencies:
|
|||||||
'@tiptap/suggestion':
|
'@tiptap/suggestion':
|
||||||
specifier: ^2.1.12
|
specifier: ^2.1.12
|
||||||
version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
|
version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
|
||||||
|
cobe:
|
||||||
|
specifier: ^0.6.3
|
||||||
|
version: 0.6.3
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.10
|
specifier: ^1.11.10
|
||||||
version: 1.11.10
|
version: 1.11.10
|
||||||
@ -188,9 +191,15 @@ dependencies:
|
|||||||
react-hotkeys-hook:
|
react-hotkeys-hook:
|
||||||
specifier: ^4.4.1
|
specifier: ^4.4.1
|
||||||
version: 4.4.1(react-dom@18.2.0)(react@18.2.0)
|
version: 4.4.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-medium-image-zoom:
|
||||||
|
specifier: ^5.1.8
|
||||||
|
version: 5.1.8(react-dom@18.2.0)(react@18.2.0)
|
||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^6.18.0
|
specifier: ^6.18.0
|
||||||
version: 6.18.0(react-dom@18.2.0)(react@18.2.0)
|
version: 6.18.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-string-replace:
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1
|
||||||
reactflow:
|
reactflow:
|
||||||
specifier: ^11.9.4
|
specifier: ^11.9.4
|
||||||
version: 11.9.4(@types/react@18.2.34)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
|
version: 11.9.4(@types/react@18.2.34)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -3381,6 +3390,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
|
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
/cobe@0.6.3:
|
||||||
|
resolution: {integrity: sha512-WHr7X4o1ym94GZ96h7b1pNemZJacbOzd02dZtnVwuC4oWBaLg96PBmp2rIS1SAhUDhhC/QyS9WEqkpZIs/ZBTg==}
|
||||||
|
dependencies:
|
||||||
|
phenomenon: 1.6.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/color-convert@1.9.3:
|
/color-convert@1.9.3:
|
||||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5153,6 +5168,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/phenomenon@1.6.0:
|
||||||
|
resolution: {integrity: sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/picocolors@1.0.0:
|
/picocolors@1.0.0:
|
||||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||||
|
|
||||||
@ -5530,6 +5549,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/react-medium-image-zoom@5.1.8(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-2X4oLlEopIWg7qalR1Qpy4gPrU9CTF0DvJ7HNu5u/NwdyQWupEsje2vuMbjBz7+np8MmQ4DKJ6zGr1ofCuzB3g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-remove-scroll-bar@2.3.4(@types/react@18.2.34)(react@18.2.0):
|
/react-remove-scroll-bar@2.3.4(@types/react@18.2.34)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
|
resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -5588,6 +5617,11 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-string-replace@1.1.1:
|
||||||
|
resolution: {integrity: sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==}
|
||||||
|
engines: {node: '>=0.12.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-style-singleton@2.2.1(@types/react@18.2.34)(react@18.2.0):
|
/react-style-singleton@2.2.1(@types/react@18.2.34)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
BIN
public/fallback-image.jpg
Normal file
BIN
public/fallback-image.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
112
src/app.css
112
src/app.css
@ -64,3 +64,115 @@ input::-ms-clear {
|
|||||||
.ProseMirror img.ProseMirror-selectednode {
|
.ProseMirror img.ProseMirror-selectednode {
|
||||||
@apply outline-blue-500;
|
@apply outline-blue-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-rmiz] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-ghost] {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-btn-zoom],
|
||||||
|
[data-rmiz-btn-unzoom] {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0;
|
||||||
|
outline-offset: 2px;
|
||||||
|
padding: 9px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
width: 40px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-btn-zoom]:not(:focus):not(:active) {
|
||||||
|
position: absolute;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-btn-zoom] {
|
||||||
|
position: absolute;
|
||||||
|
inset: 10px 10px auto auto;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-btn-unzoom] {
|
||||||
|
position: absolute;
|
||||||
|
inset: 20px 20px auto auto;
|
||||||
|
cursor: zoom-out;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-content="found"] img,
|
||||||
|
[data-rmiz-content="found"] svg,
|
||||||
|
[data-rmiz-content="found"] [role="img"],
|
||||||
|
[data-rmiz-content="found"] [data-zoom] {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-modal]::backdrop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-modal][open] {
|
||||||
|
position: fixed;
|
||||||
|
width: 100vw;
|
||||||
|
width: 100dvw;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-modal-overlay] {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-modal-overlay="hidden"] {
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-modal-overlay="visible"] {
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-modal-content] {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-rmiz-modal-img] {
|
||||||
|
position: absolute;
|
||||||
|
cursor: zoom-out;
|
||||||
|
image-rendering: high-quality;
|
||||||
|
transform-origin: top left;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
[data-rmiz-modal-overlay],
|
||||||
|
[data-rmiz-modal-img] {
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -60,7 +60,7 @@ export function ChildNote({ id, root }: { id: string; root?: string }) {
|
|||||||
Lume cannot find this post with your current relay set, but you can view
|
Lume cannot find this post with your current relay set, but you can view
|
||||||
it via njump.me
|
it via njump.me
|
||||||
</div>
|
</div>
|
||||||
<LinkPreview urls={[noteLink]} />
|
<LinkPreview url={noteLink} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -75,7 +75,7 @@ export function ChildNote({ id, root }: { id: string; root?: string }) {
|
|||||||
<User pubkey={data.pubkey} time={data.created_at} eventId={data.id} />
|
<User pubkey={data.pubkey} time={data.created_at} eventId={data.id} />
|
||||||
<div className="-mt-4 flex items-start gap-3">
|
<div className="-mt-4 flex items-start gap-3">
|
||||||
<div className="w-10 shrink-0" />
|
<div className="w-10 shrink-0" />
|
||||||
<div className="relative z-20 flex-1">
|
<div className="relative z-20 min-w-0 flex-1">
|
||||||
{renderKind(data)}
|
{renderKind(data)}
|
||||||
<NoteActions id={data.id} pubkey={data.pubkey} root={root} />
|
<NoteActions id={data.id} pubkey={data.pubkey} root={root} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,13 +52,7 @@ export function FileNote(props: { event?: NDKEvent }) {
|
|||||||
key={url}
|
key={url}
|
||||||
className="mt-2 aspect-video w-full overflow-hidden rounded-lg"
|
className="mt-2 aspect-video w-full overflow-hidden rounded-lg"
|
||||||
>
|
>
|
||||||
<video
|
<video slot="media" src={url} preload="metadata" muted />
|
||||||
slot="media"
|
|
||||||
src={url}
|
|
||||||
poster={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
|
|
||||||
preload="none"
|
|
||||||
muted
|
|
||||||
/>
|
|
||||||
<MediaControlBar>
|
<MediaControlBar>
|
||||||
<MediaPlayButton></MediaPlayButton>
|
<MediaPlayButton></MediaPlayButton>
|
||||||
<MediaTimeRange></MediaTimeRange>
|
<MediaTimeRange></MediaTimeRange>
|
||||||
@ -72,7 +66,7 @@ export function FileNote(props: { event?: NDKEvent }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<LinkPreview urls={[url]} />
|
<LinkPreview url={url} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ export function Repost({ event }: { event: NDKEvent }) {
|
|||||||
|
|
||||||
if (status === 'pending') {
|
if (status === 'pending') {
|
||||||
return (
|
return (
|
||||||
<div className="h-min w-full px-3 pb-3">
|
<div className="w-full px-3 pb-3">
|
||||||
<div className="relative overflow-hidden rounded-xl border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
|
<div className="relative overflow-hidden rounded-xl border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
</div>
|
</div>
|
||||||
@ -105,7 +105,7 @@ export function Repost({ event }: { event: NDKEvent }) {
|
|||||||
Lume cannot find this post with your current relay set, but you can view
|
Lume cannot find this post with your current relay set, but you can view
|
||||||
it via njump.me
|
it via njump.me
|
||||||
</div>
|
</div>
|
||||||
<LinkPreview urls={[noteLink]} />
|
<LinkPreview url={noteLink} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -122,7 +122,7 @@ export function Repost({ event }: { event: NDKEvent }) {
|
|||||||
<User pubkey={data.pubkey} time={data.created_at} eventId={data.id} />
|
<User pubkey={data.pubkey} time={data.created_at} eventId={data.id} />
|
||||||
<div className="-mt-4 flex items-start gap-3">
|
<div className="-mt-4 flex items-start gap-3">
|
||||||
<div className="w-10 shrink-0" />
|
<div className="w-10 shrink-0" />
|
||||||
<div className="relative z-20 flex-1">
|
<div className="relative z-20 min-w-0 flex-1">
|
||||||
{renderKind(data)}
|
{renderKind(data)}
|
||||||
<NoteActions id={data.id} pubkey={data.pubkey} />
|
<NoteActions id={data.id} pubkey={data.pubkey} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,67 +1,28 @@
|
|||||||
import Markdown from 'markdown-to-jsx';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import {
|
import { ImagePreview, LinkPreview, VideoPreview } from '@shared/notes';
|
||||||
Boost,
|
|
||||||
Hashtag,
|
|
||||||
ImagePreview,
|
|
||||||
Invoice,
|
|
||||||
LinkPreview,
|
|
||||||
MentionNote,
|
|
||||||
MentionUser,
|
|
||||||
VideoPreview,
|
|
||||||
} from '@shared/notes';
|
|
||||||
|
|
||||||
import { parser } from '@utils/parser';
|
import { useRichContent } from '@utils/hooks/useRichContent';
|
||||||
|
|
||||||
export function TextNote(props: { content?: string; truncate?: boolean }) {
|
export function TextNote(props: { content?: string; truncate?: boolean }) {
|
||||||
const richContent = parser(props.content);
|
const { parsedContent, images, videos, linkPreview } = useRichContent(props.content);
|
||||||
|
|
||||||
if (props.truncate) {
|
if (props.truncate) {
|
||||||
return (
|
return (
|
||||||
<div className="break-p prose prose-neutral line-clamp-4 max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-600 prose-a:hover:underline">
|
<div className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert">
|
||||||
{props.content}
|
{props.content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col items-start gap-2">
|
<div className="flex flex-col gap-3">
|
||||||
<Markdown
|
<div className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-img:mb-0 prose-img:mt-0">
|
||||||
options={{
|
{parsedContent}
|
||||||
overrides: {
|
</div>
|
||||||
Hashtag: {
|
{images.length ? <ImagePreview urls={images} /> : null}
|
||||||
component: Hashtag,
|
{videos.length ? <VideoPreview urls={videos} /> : null}
|
||||||
},
|
{linkPreview ? <LinkPreview url={linkPreview} /> : null}
|
||||||
Boost: {
|
|
||||||
component: Boost,
|
|
||||||
},
|
|
||||||
MentionUser: {
|
|
||||||
component: MentionUser,
|
|
||||||
},
|
|
||||||
Invoice: {
|
|
||||||
component: Invoice,
|
|
||||||
},
|
|
||||||
a: {
|
|
||||||
props: {
|
|
||||||
target: '_blank',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
slugify: (str) => str,
|
|
||||||
forceBlock: true,
|
|
||||||
enforceAtxHeadings: true,
|
|
||||||
}}
|
|
||||||
className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-600 prose-a:hover:underline"
|
|
||||||
>
|
|
||||||
{richContent.parsed}
|
|
||||||
</Markdown>
|
|
||||||
{richContent.images.length ? <ImagePreview urls={richContent.images} /> : null}
|
|
||||||
{richContent.videos.length ? <VideoPreview urls={richContent.videos} /> : null}
|
|
||||||
{richContent.links.length ? <LinkPreview urls={richContent.links} /> : null}
|
|
||||||
{richContent.notes.map((note: string) => (
|
|
||||||
<MentionNote key={note} id={note} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
|
|||||||
|
|
||||||
export function UnknownNote(props: { event?: NDKEvent }) {
|
export function UnknownNote(props: { event?: NDKEvent }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="mt-2 flex w-full flex-col gap-2">
|
||||||
<div className="inline-flex flex-col rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
<div className="inline-flex flex-col rounded-md border border-neutral-300 bg-neutral-200 px-2 py-2 dark:border-neutral-700 dark:bg-neutral-800">
|
||||||
<span className="text-sm font-medium text-neutral-500 dark:text-neutral-400">
|
<span className="text-sm font-medium text-neutral-500 dark:text-neutral-400">
|
||||||
Kind: {props.event.kind}
|
Kind: {props.event.kind}
|
||||||
</span>
|
</span>
|
||||||
@ -12,7 +12,7 @@ export function UnknownNote(props: { event?: NDKEvent }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="select-text whitespace-pre-line break-words text-neutral-800 dark:text-neutral-200">
|
<div className="select-text whitespace-pre-line break-words text-neutral-800 dark:text-neutral-200">
|
||||||
<p>{props.event.content.toString()}</p>
|
{props.event.content.toString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -49,7 +49,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
|||||||
|
|
||||||
if (status === 'pending') {
|
if (status === 'pending') {
|
||||||
return (
|
return (
|
||||||
<div className="w-full cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
|
<div className="my-2 w-full cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -58,17 +58,15 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
|||||||
if (status === 'error') {
|
if (status === 'error') {
|
||||||
const noteLink = `https://njump.me/${nip19.noteEncode(id)}`;
|
const noteLink = `https://njump.me/${nip19.noteEncode(id)}`;
|
||||||
return (
|
return (
|
||||||
<div className="w-full rounded-lg bg-neutral-200 px-3 py-3 dark:bg-neutral-800">
|
<div className="my-2 w-full rounded-lg bg-neutral-200 px-3 py-3 dark:bg-neutral-800">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="inline-flex h-6 w-6 items-end justify-center rounded bg-black pb-1">
|
<div className="inline-flex h-6 w-6 items-end justify-center rounded-md bg-black pb-1"></div>
|
||||||
<img src="/lume.png" alt="lume" className="h-auto w-1/3" />
|
<h5 className="truncate font-semibold">
|
||||||
</div>
|
|
||||||
<h5 className="truncate font-semibold leading-none text-white">
|
|
||||||
Lume <span className="text-green-500">(System)</span>
|
Lume <span className="text-green-500">(System)</span>
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1.5">
|
<div className="mt-1.5">
|
||||||
<LinkPreview urls={[noteLink]} />
|
<LinkPreview url={noteLink} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -79,7 +77,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
|||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
onClick={(e) => openThread(e, id)}
|
onClick={(e) => openThread(e, id)}
|
||||||
className="w-full cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800"
|
className="my-2 w-full cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
|
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
|
||||||
<div className="mt-1 text-left">{renderKind(data)}</div>
|
<div className="mt-1 text-left">{renderKind(data)}</div>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { downloadDir } from '@tauri-apps/api/path';
|
import { downloadDir } from '@tauri-apps/api/path';
|
||||||
import { download } from '@tauri-apps/plugin-upload';
|
import { download } from '@tauri-apps/plugin-upload';
|
||||||
|
import { SyntheticEvent } from 'react';
|
||||||
|
import Zoom from 'react-medium-image-zoom';
|
||||||
|
|
||||||
import { DownloadIcon } from '@shared/icons';
|
import { DownloadIcon } from '@shared/icons';
|
||||||
|
|
||||||
@ -10,23 +12,33 @@ export function ImagePreview({ urls }: { urls: string[] }) {
|
|||||||
return await download(url, downloadDirPath + `/${filename}`);
|
return await download(url, downloadDirPath + `/${filename}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fallback = (event: SyntheticEvent<HTMLImageElement, Event>) => {
|
||||||
|
event.currentTarget.src = '/fallback-image.jpg';
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
{urls.map((url) => (
|
{urls.map((url) => (
|
||||||
<div key={url} className="group relative">
|
<Zoom key={url} zoomMargin={50}>
|
||||||
<img
|
<div className="group relative">
|
||||||
src={url}
|
<img
|
||||||
alt={url}
|
src={url}
|
||||||
className="h-auto w-full rounded-lg border border-neutral-300 object-cover dark:border-neutral-700"
|
alt={url}
|
||||||
/>
|
loading="lazy"
|
||||||
<button
|
decoding="async"
|
||||||
type="button"
|
style={{ contentVisibility: 'auto' }}
|
||||||
onClick={() => downloadImage(url)}
|
onError={fallback}
|
||||||
className="absolute right-2 top-2 hidden h-10 w-10 items-center justify-center rounded-lg bg-black/50 backdrop-blur-xl group-hover:inline-flex hover:bg-blue-500"
|
className="h-auto w-full rounded-lg border border-neutral-300 object-cover dark:border-neutral-700"
|
||||||
>
|
/>
|
||||||
<DownloadIcon className="h-5 w-5 text-white" />
|
<button
|
||||||
</button>
|
type="button"
|
||||||
</div>
|
onClick={() => downloadImage(url)}
|
||||||
|
className="absolute right-2 top-2 hidden h-10 w-10 items-center justify-center rounded-xl bg-black/50 backdrop-blur-xl group-hover:inline-flex hover:bg-blue-500"
|
||||||
|
>
|
||||||
|
<DownloadIcon className="h-4 w-4 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Zoom>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,9 +6,9 @@ function isImage(url: string) {
|
|||||||
return /^https?:\/\/.+\.(jpg|jpeg|png|webp|avif)$/.test(url);
|
return /^https?:\/\/.+\.(jpg|jpeg|png|webp|avif)$/.test(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LinkPreview({ urls }: { urls: string[] }) {
|
export function LinkPreview({ url }: { url: string }) {
|
||||||
const { status, data } = useOpenGraph(urls[0]);
|
const domain = new URL(url);
|
||||||
const domain = new URL(urls[0]);
|
const { status, data } = useOpenGraph(url);
|
||||||
|
|
||||||
if (status === 'pending') {
|
if (status === 'pending') {
|
||||||
return (
|
return (
|
||||||
@ -27,7 +27,7 @@ export function LinkPreview({ urls }: { urls: string[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={urls[0]}
|
to={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="flex w-full flex-col rounded-lg border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
|
className="flex w-full flex-col rounded-lg border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
|
||||||
@ -35,7 +35,7 @@ export function LinkPreview({ urls }: { urls: string[] }) {
|
|||||||
{isImage(data.image) ? (
|
{isImage(data.image) ? (
|
||||||
<img
|
<img
|
||||||
src={data.image}
|
src={data.image}
|
||||||
alt={urls[0]}
|
alt={url}
|
||||||
className="h-44 w-full rounded-t-lg bg-white object-cover"
|
className="h-44 w-full rounded-t-lg bg-white object-cover"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -7,20 +7,13 @@ import {
|
|||||||
MediaTimeRange,
|
MediaTimeRange,
|
||||||
MediaVolumeRange,
|
MediaVolumeRange,
|
||||||
} from 'media-chrome/dist/react';
|
} from 'media-chrome/dist/react';
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
export const VideoPreview = memo(function VideoPreview({ urls }: { urls: string[] }) {
|
export function VideoPreview({ urls }: { urls: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
{urls.map((url) => (
|
{urls.map((url) => (
|
||||||
<MediaController key={url} className="aspect-video overflow-hidden rounded-lg">
|
<MediaController key={url} className="aspect-video overflow-hidden rounded-lg">
|
||||||
<video
|
<video slot="media" src={url} preload="metadata" muted />
|
||||||
slot="media"
|
|
||||||
src={url}
|
|
||||||
poster={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
|
|
||||||
preload="none"
|
|
||||||
muted
|
|
||||||
/>
|
|
||||||
<MediaControlBar>
|
<MediaControlBar>
|
||||||
<MediaPlayButton></MediaPlayButton>
|
<MediaPlayButton></MediaPlayButton>
|
||||||
<MediaTimeRange></MediaTimeRange>
|
<MediaTimeRange></MediaTimeRange>
|
||||||
@ -32,4 +25,4 @@ export const VideoPreview = memo(function VideoPreview({ urls }: { urls: string[
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
@ -29,7 +29,7 @@ export function NoteWrapper({
|
|||||||
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
||||||
<div className="-mt-4 flex items-start gap-3">
|
<div className="-mt-4 flex items-start gap-3">
|
||||||
<div className="w-10 shrink-0" />
|
<div className="w-10 shrink-0" />
|
||||||
<div className="relative z-20 flex-1">
|
<div className="relative z-20 min-w-0 flex-1">
|
||||||
{cloneElement(
|
{cloneElement(
|
||||||
children,
|
children,
|
||||||
event.kind === 1 ? { content: event.content } : { event: event }
|
event.kind === 1 ? { content: event.content } : { event: event }
|
||||||
|
@ -54,7 +54,7 @@ export const User = memo(function User({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex h-6 items-center gap-2">
|
||||||
<Avatar.Root className="shrink-0">
|
<Avatar.Root className="shrink-0">
|
||||||
<Avatar.Image
|
<Avatar.Image
|
||||||
src={user?.picture || user?.image}
|
src={user?.picture || user?.image}
|
||||||
|
@ -19,8 +19,6 @@ import {
|
|||||||
import { TitleBar } from '@shared/titleBar';
|
import { TitleBar } from '@shared/titleBar';
|
||||||
import { WidgetWrapper } from '@shared/widgets';
|
import { WidgetWrapper } from '@shared/widgets';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
|
||||||
|
|
||||||
export function NewsfeedWidget() {
|
export function NewsfeedWidget() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@ -139,6 +137,8 @@ export function NewsfeedWidget() {
|
|||||||
};
|
};
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
|
console.log('RERENDER');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WidgetWrapper>
|
<WidgetWrapper>
|
||||||
<TitleBar id="9999" isLive />
|
<TitleBar id="9999" isLive />
|
||||||
|
152
src/utils/hooks/useRichContent.tsx
Normal file
152
src/utils/hooks/useRichContent.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import reactStringReplace from 'react-string-replace';
|
||||||
|
|
||||||
|
import { Hashtag, MentionNote, MentionUser } from '@shared/notes';
|
||||||
|
|
||||||
|
const NOSTR_MENTIONS = [
|
||||||
|
'@npub1',
|
||||||
|
'nostr:npub1',
|
||||||
|
'nostr:nprofile1',
|
||||||
|
'nostr:naddr1',
|
||||||
|
'npub1',
|
||||||
|
'nprofile1',
|
||||||
|
'naddr1',
|
||||||
|
];
|
||||||
|
|
||||||
|
const NOSTR_EVENTS = ['nostr:note1', 'note1', 'nostr:nevent1', 'nevent1'];
|
||||||
|
|
||||||
|
// const BITCOINS = ['lnbc', 'bc1p', 'bc1q'];
|
||||||
|
|
||||||
|
const IMAGES = ['.jpg', '.jpeg', '.gif', '.png', '.webp', '.avif', '.tiff'];
|
||||||
|
|
||||||
|
const VIDEOS = [
|
||||||
|
'.mp4',
|
||||||
|
'.mov',
|
||||||
|
'.webm',
|
||||||
|
'.wmv',
|
||||||
|
'.flv',
|
||||||
|
'.mts',
|
||||||
|
'.avi',
|
||||||
|
'.ogv',
|
||||||
|
'.mkv',
|
||||||
|
'.mp3',
|
||||||
|
'.m3u8',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function useRichContent(content: string) {
|
||||||
|
let parsedContent: string | ReactNode[] = content;
|
||||||
|
let linkPreview: string;
|
||||||
|
|
||||||
|
const text = content;
|
||||||
|
const words = text.split(/(\s+)/);
|
||||||
|
|
||||||
|
const images = words
|
||||||
|
.filter((word) => IMAGES.some((el) => word.endsWith(el)))
|
||||||
|
.map((item: string) => item);
|
||||||
|
|
||||||
|
const videos = words
|
||||||
|
.filter((word) => VIDEOS.some((el) => word.endsWith(el)))
|
||||||
|
.map((item: string) => item);
|
||||||
|
|
||||||
|
const hashtags = words.filter((word) => word.startsWith('#'));
|
||||||
|
const events = words.filter((word) => NOSTR_EVENTS.some((el) => word.startsWith(el)));
|
||||||
|
|
||||||
|
const mentions = words.filter((word) =>
|
||||||
|
NOSTR_MENTIONS.some((el) => word.startsWith(el))
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (images.length) {
|
||||||
|
images.forEach((image) => {
|
||||||
|
// @ts-expect-error, it is string at this time
|
||||||
|
parsedContent = parsedContent.replace(image, '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videos.length) {
|
||||||
|
videos.forEach((video) => {
|
||||||
|
// @ts-expect-error, it is string at this time
|
||||||
|
parsedContent = parsedContent.replace(video, '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashtags.length) {
|
||||||
|
hashtags.forEach((hashtag) => {
|
||||||
|
parsedContent = reactStringReplace(parsedContent, hashtag, (match, i) => (
|
||||||
|
<Hashtag key={match + i} tag={hashtag} />
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.length) {
|
||||||
|
events.forEach((event) => {
|
||||||
|
const address = event.replace('nostr:', '');
|
||||||
|
const decoded = nip19.decode(address);
|
||||||
|
|
||||||
|
if (decoded.type === 'note') {
|
||||||
|
parsedContent = reactStringReplace(parsedContent, event, (match, i) => (
|
||||||
|
<MentionNote key={match + i} id={decoded.data} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoded.type === 'nevent') {
|
||||||
|
parsedContent = reactStringReplace(parsedContent, event, (match, i) => (
|
||||||
|
<MentionNote key={match + i} id={decoded.data.id} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mentions.length) {
|
||||||
|
mentions.forEach((mention) => {
|
||||||
|
const address = mention.replace('nostr:', '').replace('@', '');
|
||||||
|
const decoded = nip19.decode(address);
|
||||||
|
|
||||||
|
if (decoded.type === 'npub') {
|
||||||
|
parsedContent = reactStringReplace(parsedContent, mention, (match, i) => (
|
||||||
|
<MentionUser key={match + i} pubkey={decoded.data} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoded.type === 'nprofile' || decoded.type === 'naddr') {
|
||||||
|
parsedContent = reactStringReplace(parsedContent, mention, (match, i) => (
|
||||||
|
<MentionUser key={match + i} pubkey={decoded.data.pubkey} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => {
|
||||||
|
if (!linkPreview) {
|
||||||
|
linkPreview = match;
|
||||||
|
if (content.length < 500) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(match);
|
||||||
|
url.search = '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={match + i}
|
||||||
|
to={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="break-all font-normal text-blue-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
{url.toString()}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof parsedContent[0] === 'string') {
|
||||||
|
parsedContent[0] = parsedContent[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { parsedContent, images, videos, linkPreview };
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[parser] parse failed: ', e);
|
||||||
|
return { parsedContent, images, videos, linkPreview };
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user