feat: render markdown
This commit is contained in:
149
src/Text.js
149
src/Text.js
@ -1,4 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { TwitterTweetEmbed } from "react-twitter-embed";
|
||||
|
||||
import Invoice from "./element/Invoice";
|
||||
@ -6,64 +9,59 @@ import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlReg
|
||||
import { eventLink, hexToBech32, profileLink } from "./Util";
|
||||
import LazyImage from "./element/LazyImage";
|
||||
import Hashtag from "./element/Hashtag";
|
||||
import { useMemo } from "react";
|
||||
|
||||
function transformHttpLink(a) {
|
||||
try {
|
||||
const url = new URL(a);
|
||||
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1;
|
||||
const tweetId = TweetUrlRegex.test(a) && RegExp.$2;
|
||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||
if (extension) {
|
||||
switch (extension) {
|
||||
case "gif":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "webp": {
|
||||
return <LazyImage key={url} src={url} />;
|
||||
}
|
||||
case "mp4":
|
||||
case "mov":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "m4v": {
|
||||
return <video key={url} src={url} controls />
|
||||
}
|
||||
default:
|
||||
return <a key={url} href={url} onClick={(e) => e.stopPropagation()}>{url.toString()}</a>
|
||||
const url = new URL(a);
|
||||
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1;
|
||||
const tweetId = TweetUrlRegex.test(a) && RegExp.$2;
|
||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||
if (extension) {
|
||||
switch (extension) {
|
||||
case "gif":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "webp": {
|
||||
return <LazyImage key={url} src={url} />;
|
||||
}
|
||||
} else if (tweetId) {
|
||||
return (
|
||||
<div className="tweet">
|
||||
<TwitterTweetEmbed tweetId={tweetId} />
|
||||
</div>
|
||||
)
|
||||
} else if (youtubeId) {
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<iframe
|
||||
className="w-max"
|
||||
src={`https://www.youtube.com/embed/${youtubeId}`}
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen=""
|
||||
/>
|
||||
<br />
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return <a key={url} href={url} onClick={(e) => e.stopPropagation()}>{url.toString()}</a>
|
||||
case "mp4":
|
||||
case "mov":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "m4v": {
|
||||
return <video key={url} src={url} controls />
|
||||
}
|
||||
default:
|
||||
return <a key={url} href={url} onClick={(e) => e.stopPropagation()}>{url.toString()}</a>
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Not a valid url: ${a}`);
|
||||
} else if (tweetId) {
|
||||
return (
|
||||
<div className="tweet">
|
||||
<TwitterTweetEmbed tweetId={tweetId} />
|
||||
</div>
|
||||
)
|
||||
} else if (youtubeId) {
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<iframe
|
||||
className="w-max"
|
||||
src={`https://www.youtube.com/embed/${youtubeId}`}
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen=""
|
||||
/>
|
||||
<br />
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return <a href={a} onClick={(e) => e.stopPropagation()}>{a}</a>
|
||||
}
|
||||
}
|
||||
|
||||
export function extractLinks(fragments) {
|
||||
function extractLinks(fragments) {
|
||||
return fragments.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(UrlRegex).map(a => {
|
||||
@ -107,7 +105,7 @@ export function extractMentions(fragments, tags, users) {
|
||||
}).flat();
|
||||
}
|
||||
|
||||
export function extractInvoices(fragments) {
|
||||
function extractInvoices(fragments) {
|
||||
return fragments.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(InvoiceRegex).map(i => {
|
||||
@ -122,7 +120,7 @@ export function extractInvoices(fragments) {
|
||||
}).flat();
|
||||
}
|
||||
|
||||
export function extractHashtags(fragments) {
|
||||
function extractHashtags(fragments) {
|
||||
return fragments.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(HashtagRegex).map(i => {
|
||||
@ -137,18 +135,37 @@ export function extractHashtags(fragments) {
|
||||
}).flat();
|
||||
}
|
||||
|
||||
function transformLi({ body, transforms }) {
|
||||
let fragments = transformText({ body, transforms })
|
||||
return <li>{fragments}</li>
|
||||
}
|
||||
|
||||
function transformParagraph({ body, transforms }) {
|
||||
const fragments = transformText({ body, transforms })
|
||||
if (fragments.every(f => typeof f === 'string')) {
|
||||
return <p>{fragments}</p>
|
||||
}
|
||||
return <>{fragments}</>
|
||||
}
|
||||
|
||||
function transformText({ body, transforms }) {
|
||||
let fragments = [body];
|
||||
transforms?.forEach(a => {
|
||||
fragments = a(fragments);
|
||||
});
|
||||
fragments = extractLinks(fragments);
|
||||
fragments = extractInvoices(fragments);
|
||||
fragments = extractHashtags(fragments);
|
||||
|
||||
return fragments;
|
||||
}
|
||||
|
||||
export default function Text({ content, transforms }) {
|
||||
const transformed = useMemo(() => {
|
||||
let fragments = [content];
|
||||
transforms?.forEach(a => {
|
||||
fragments = a(fragments);
|
||||
});
|
||||
fragments = extractLinks(fragments);
|
||||
fragments = extractInvoices(fragments);
|
||||
fragments = extractHashtags(fragments);
|
||||
const components = {
|
||||
p: (props) => transformParagraph({ body: props.children, transforms }),
|
||||
a: (props) => transformHttpLink(props.href),
|
||||
li: (props) => transformLi({ body: props.children, transforms }),
|
||||
}
|
||||
return <ReactMarkdown components={components}>{content}</ReactMarkdown>
|
||||
}
|
||||
|
||||
return fragments;
|
||||
}, [content]);
|
||||
|
||||
return transformed;
|
||||
}
|
@ -15,4 +15,4 @@ export default function LazyImage(props) {
|
||||
}, [inView]);
|
||||
|
||||
return shown ? <img {...props} /> : <div ref={ref}></div>
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,36 @@
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.note > .body h1 {
|
||||
margin: 0;
|
||||
}
|
||||
.note > .body h2 {
|
||||
margin: 0;
|
||||
}
|
||||
.note > .body h3 {
|
||||
margin: 0;
|
||||
}
|
||||
.note > .body h4 {
|
||||
margin: 0;
|
||||
}
|
||||
.note > .body h5 {
|
||||
margin: 0;
|
||||
}
|
||||
.note > .body h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.note > .body > p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.note > .body > pre {
|
||||
}
|
||||
|
||||
.note > .body > ul, .note > .body > ol {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.note > .body img, .note > .body video, .note > .body iframe {
|
||||
max-width: 100%;
|
||||
max-height: 500px;
|
||||
|
@ -88,4 +88,4 @@ export default function Note(props) {
|
||||
{options.showFooter ? <NoteFooter ev={ev} reactions={reactions} /> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@
|
||||
|
||||
.profile .name h2 {
|
||||
margin: 0;
|
||||
=======
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
@ -37,10 +36,8 @@
|
||||
height: 300px;
|
||||
margin-bottom: -120px;
|
||||
}
|
||||
>>>>>>> c68c73a (feat: display banner in profile)
|
||||
}
|
||||
|
||||
|
||||
.profile .avatar-wrapper {
|
||||
align-self: flex-start;
|
||||
z-index: 1;
|
||||
|
@ -11,7 +11,7 @@ import useProfile from "../feed/ProfileFeed";
|
||||
import FollowButton from "../element/FollowButton";
|
||||
import { extractLnAddress, parseId, hexToBech32 } from "../Util";
|
||||
import Timeline from "../element/Timeline";
|
||||
import { extractLinks, extractHashtags } from '../Text'
|
||||
import Text, { mentions } from '../Text'
|
||||
import LNURLTip from "../element/LNURLTip";
|
||||
import Nip05, { useIsVerified } from "../element/Nip05";
|
||||
import Copy from "../element/Copy";
|
||||
@ -36,7 +36,7 @@ export default function ProfilePage() {
|
||||
const isMe = loginPubKey === id;
|
||||
const [showLnQr, setShowLnQr] = useState(false);
|
||||
const [tab, setTab] = useState(ProfileTab.Notes);
|
||||
const about = extractHashtags(extractLinks([user?.about]))
|
||||
const about = Text({ content: user?.about })
|
||||
const { name, domain, isVerified, couldNotVerify } = useIsVerified(user?.nip05, user?.pubkey)
|
||||
const avatarUrl = (user?.picture?.length ?? 0) === 0 ? Nostrich : user?.picture
|
||||
const backgroundImage = `url(${avatarUrl})`
|
||||
|
Reference in New Issue
Block a user