feat: render markdown

This commit is contained in:
Alejandro Gomez
2023-01-14 02:39:20 +01:00
parent 80024bc391
commit e0957deca8
8 changed files with 624 additions and 76 deletions

View File

@ -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;
}

View File

@ -15,4 +15,4 @@ export default function LazyImage(props) {
}, [inView]);
return shown ? <img {...props} /> : <div ref={ref}></div>
}
}

View File

@ -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;

View File

@ -88,4 +88,4 @@ export default function Note(props) {
{options.showFooter ? <NoteFooter ev={ev} reactions={reactions} /> : null}
</div>
)
}
}

View File

@ -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;

View File

@ -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})`