bug: mentions duplication
This commit is contained in:
parent
b650a1684f
commit
c79adf7e9e
@ -95,7 +95,7 @@ export const MnemonicRegex = /^([^\s]+\s){11}[^\s]+$/;
|
||||
* Extract file extensions regex
|
||||
*/
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
export const FileExtensionRegex = /\.([\w]+)$/i;
|
||||
export const FileExtensionRegex = /\.([\w]{1,7})$/i;
|
||||
|
||||
/**
|
||||
* Extract note reactions regex
|
||||
|
@ -1,9 +1,5 @@
|
||||
import { useCallback, useState, Children } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { TwitterTweetEmbed } from "react-twitter-embed";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import {
|
||||
FileExtensionRegex,
|
||||
YoutubeUrlRegex,
|
||||
@ -17,161 +13,100 @@ import {
|
||||
NostrNestsRegex,
|
||||
WavlakeRegex,
|
||||
} from "Const";
|
||||
import { RootState } from "State/Store";
|
||||
import { magnetURIDecode } from "Util";
|
||||
import SoundCloudEmbed from "Element/SoundCloudEmded";
|
||||
import MixCloudEmbed from "Element/MixCloudEmbed";
|
||||
import SpotifyEmbed from "Element/SpotifyEmbed";
|
||||
import TidalEmbed from "Element/TidalEmbed";
|
||||
import { ProxyImg } from "Element/ProxyImg";
|
||||
import TwitchEmbed from "Element/TwitchEmbed";
|
||||
import AppleMusicEmbed from "Element/AppleMusicEmbed";
|
||||
import NostrNestsEmbed from "Element/NostrNestsEmbed";
|
||||
import WavlakeEmbed from "Element/WavlakeEmbed";
|
||||
import LinkPreview from "Element/LinkPreview";
|
||||
import NostrLink from "Element/NostrLink";
|
||||
import RevealMedia from "Element/RevealMedia";
|
||||
import MagnetLink from "Element/MagnetLink";
|
||||
|
||||
export default function HyperText({ link, creator }: { link: string; creator: HexKey }) {
|
||||
const pref = useSelector((s: RootState) => s.login.preferences);
|
||||
const follows = useSelector((s: RootState) => s.login.follows);
|
||||
const publicKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
const [reveal, setReveal] = useState(false);
|
||||
|
||||
const wrapReveal = useCallback(
|
||||
(e: JSX.Element, a: string): JSX.Element => {
|
||||
const hideNonFollows = pref.autoLoadMedia === "follows-only" && !follows.includes(creator);
|
||||
const isMine = creator === publicKey;
|
||||
const hideMedia = pref.autoLoadMedia === "none" || (!isMine && hideNonFollows);
|
||||
const hostname = new URL(a).host;
|
||||
|
||||
if (hideMedia && !reveal) {
|
||||
return (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setReveal(true);
|
||||
}}
|
||||
className="note-invoice">
|
||||
<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: hostname }} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
[reveal, pref, follows, publicKey]
|
||||
);
|
||||
|
||||
const render = useCallback(() => {
|
||||
const a = link;
|
||||
try {
|
||||
const url = new URL(a);
|
||||
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1;
|
||||
const tweetId = TweetUrlRegex.test(a) && RegExp.$2;
|
||||
const tidalId = TidalRegex.test(a) && RegExp.$1;
|
||||
const soundcloundId = SoundCloudRegex.test(a) && RegExp.$1;
|
||||
const mixcloudId = MixCloudRegex.test(a) && RegExp.$1;
|
||||
const isSpotifyLink = SpotifyRegex.test(a);
|
||||
const isTwitchLink = TwitchRegex.test(a);
|
||||
const isAppleMusicLink = AppleMusicRegex.test(a);
|
||||
const isNostrNestsLink = NostrNestsRegex.test(a);
|
||||
const isWavlakeLink = WavlakeRegex.test(a);
|
||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||
if (extension && !isAppleMusicLink) {
|
||||
switch (extension) {
|
||||
case "gif":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "jfif":
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "webp": {
|
||||
return <ProxyImg key={url.toString()} src={url.toString()} />;
|
||||
}
|
||||
case "wav":
|
||||
case "mp3":
|
||||
case "ogg": {
|
||||
return <audio key={url.toString()} src={url.toString()} controls />;
|
||||
}
|
||||
case "mp4":
|
||||
case "mov":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "m4v":
|
||||
case "webm": {
|
||||
return <video key={url.toString()} src={url.toString()} controls />;
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<a
|
||||
key={url.toString()}
|
||||
href={url.toString()}
|
||||
onClick={e => e.stopPropagation()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ext">
|
||||
{url.toString()}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
} else if (tweetId) {
|
||||
return (
|
||||
<div className="tweet" key={tweetId}>
|
||||
<TwitterTweetEmbed tweetId={tweetId} />
|
||||
</div>
|
||||
);
|
||||
} else if (youtubeId) {
|
||||
return (
|
||||
<iframe
|
||||
className="w-max"
|
||||
src={`https://www.youtube.com/embed/${youtubeId}`}
|
||||
title="YouTube video player"
|
||||
key={youtubeId}
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen={true}
|
||||
/>
|
||||
);
|
||||
} else if (tidalId) {
|
||||
return <TidalEmbed link={a} />;
|
||||
} else if (soundcloundId) {
|
||||
return <SoundCloudEmbed link={a} />;
|
||||
} else if (mixcloudId) {
|
||||
return <MixCloudEmbed link={a} />;
|
||||
} else if (isSpotifyLink) {
|
||||
return <SpotifyEmbed link={a} />;
|
||||
} else if (isTwitchLink) {
|
||||
return <TwitchEmbed link={a} />;
|
||||
} else if (isAppleMusicLink) {
|
||||
return <AppleMusicEmbed link={a} />;
|
||||
} else if (isNostrNestsLink) {
|
||||
return [
|
||||
export default function HyperText({ link, creator }: { link: string; creator: string }) {
|
||||
const a = link;
|
||||
try {
|
||||
const url = new URL(a);
|
||||
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1;
|
||||
const tweetId = TweetUrlRegex.test(a) && RegExp.$2;
|
||||
const tidalId = TidalRegex.test(a) && RegExp.$1;
|
||||
const soundcloundId = SoundCloudRegex.test(a) && RegExp.$1;
|
||||
const mixcloudId = MixCloudRegex.test(a) && RegExp.$1;
|
||||
const isSpotifyLink = SpotifyRegex.test(a);
|
||||
const isTwitchLink = TwitchRegex.test(a);
|
||||
const isAppleMusicLink = AppleMusicRegex.test(a);
|
||||
const isNostrNestsLink = NostrNestsRegex.test(a);
|
||||
const isWavlakeLink = WavlakeRegex.test(a);
|
||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||
if (extension && !isAppleMusicLink) {
|
||||
return <RevealMedia link={a} creator={creator} />;
|
||||
} else if (tweetId) {
|
||||
return (
|
||||
<div className="tweet" key={tweetId}>
|
||||
<TwitterTweetEmbed tweetId={tweetId} />
|
||||
</div>
|
||||
);
|
||||
} else if (youtubeId) {
|
||||
return (
|
||||
<iframe
|
||||
className="w-max"
|
||||
src={`https://www.youtube.com/embed/${youtubeId}`}
|
||||
title="YouTube video player"
|
||||
key={youtubeId}
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen={true}
|
||||
/>
|
||||
);
|
||||
} else if (tidalId) {
|
||||
return <TidalEmbed link={a} />;
|
||||
} else if (soundcloundId) {
|
||||
return <SoundCloudEmbed link={a} />;
|
||||
} else if (mixcloudId) {
|
||||
return <MixCloudEmbed link={a} />;
|
||||
} else if (isSpotifyLink) {
|
||||
return <SpotifyEmbed link={a} />;
|
||||
} else if (isTwitchLink) {
|
||||
return <TwitchEmbed link={a} />;
|
||||
} else if (isAppleMusicLink) {
|
||||
return <AppleMusicEmbed link={a} />;
|
||||
} else if (isNostrNestsLink) {
|
||||
return (
|
||||
<>
|
||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{a}
|
||||
</a>,
|
||||
/*<NostrNestsEmbed link={a} />,*/
|
||||
];
|
||||
} else if (isWavlakeLink) {
|
||||
return <WavlakeEmbed link={a} />;
|
||||
} else if (url.protocol === "nostr:" || url.protocol === "web+nostr:") {
|
||||
return <NostrLink link={a} />;
|
||||
} else {
|
||||
return [
|
||||
</a>
|
||||
{/*<NostrNestsEmbed link={a} />,*/}
|
||||
</>
|
||||
);
|
||||
} else if (isWavlakeLink) {
|
||||
return <WavlakeEmbed link={a} />;
|
||||
} else if (url.protocol === "nostr:" || url.protocol === "web+nostr:") {
|
||||
return <NostrLink link={a} />;
|
||||
} else if (url.protocol === "magnet:") {
|
||||
const parsed = magnetURIDecode(a);
|
||||
if (parsed) {
|
||||
return <MagnetLink magnet={parsed} />;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{a}
|
||||
</a>,
|
||||
<LinkPreview url={a} />,
|
||||
];
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore the error.
|
||||
</a>
|
||||
<LinkPreview url={a} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{a}
|
||||
</a>
|
||||
);
|
||||
}, [link, reveal]);
|
||||
|
||||
const children = render();
|
||||
return <>{Children.map(children, elm => (elm.type === "a" ? elm : wrapReveal(elm, link)))}</>;
|
||||
} catch {
|
||||
// Ignore the error.
|
||||
}
|
||||
return (
|
||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{a}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
43
packages/app/src/Element/MediaLink.tsx
Normal file
43
packages/app/src/Element/MediaLink.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { FileExtensionRegex } from "Const";
|
||||
import { ProxyImg } from "Element/ProxyImg";
|
||||
|
||||
export default function MediaLink({ link }: { link: string }) {
|
||||
const url = new URL(link);
|
||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||
switch (extension) {
|
||||
case "gif":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "jfif":
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "webp": {
|
||||
return <ProxyImg key={url.toString()} src={url.toString()} />;
|
||||
}
|
||||
case "wav":
|
||||
case "mp3":
|
||||
case "ogg": {
|
||||
return <audio key={url.toString()} src={url.toString()} controls />;
|
||||
}
|
||||
case "mp4":
|
||||
case "mov":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "m4v":
|
||||
case "webm": {
|
||||
return <video key={url.toString()} src={url.toString()} controls />;
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<a
|
||||
key={url.toString()}
|
||||
href={url.toString()}
|
||||
onClick={e => e.stopPropagation()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ext">
|
||||
{url.toString()}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ import {
|
||||
} from "Util";
|
||||
import NoteFooter, { Translation } from "Element/NoteFooter";
|
||||
import NoteTime from "Element/NoteTime";
|
||||
import Reveal from "Element/Reveal";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { setPinned, setBookmarked } from "State/Login";
|
||||
import type { RootState } from "State/Store";
|
||||
@ -83,7 +84,6 @@ export default function Note(props: NoteProps) {
|
||||
const { pinned, bookmarked } = useSelector((s: RootState) => s.login);
|
||||
const publisher = useEventPublisher();
|
||||
const [translated, setTranslated] = useState<Translation>();
|
||||
const [contentWarningAccepted, setContentWarningAccepted] = useState(false);
|
||||
const { formatMessage } = useIntl();
|
||||
const reactions = useMemo(() => getReactions(related, ev.id, EventKind.Reaction), [related, ev]);
|
||||
const groupReactions = useMemo(() => {
|
||||
@ -153,7 +153,7 @@ export default function Note(props: NoteProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const transformBody = useCallback(() => {
|
||||
const transformBody = () => {
|
||||
const body = ev?.content ?? "";
|
||||
if (deletions?.length > 0) {
|
||||
return (
|
||||
@ -163,31 +163,31 @@ export default function Note(props: NoteProps) {
|
||||
);
|
||||
}
|
||||
const contentWarning = ev.tags.find(a => a[0] === "content-warning");
|
||||
if (contentWarning && !contentWarningAccepted) {
|
||||
if (contentWarning) {
|
||||
return (
|
||||
<div
|
||||
className="note-invoice"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setContentWarningAccepted(true);
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="This note has been marked as sensitive, click here to reveal" />
|
||||
{contentWarning[1] && (
|
||||
<Reveal
|
||||
message={
|
||||
<>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
defaultMessage="Reason: {reason}"
|
||||
values={{
|
||||
reason: contentWarning[1],
|
||||
}}
|
||||
/>
|
||||
<FormattedMessage defaultMessage="This note has been marked as sensitive, click here to reveal" />
|
||||
{contentWarning[1] && (
|
||||
<>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
defaultMessage="Reason: {reason}"
|
||||
values={{
|
||||
reason: contentWarning[1],
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}>
|
||||
<Text content={body} tags={ev.tags} creator={ev.pubkey} />
|
||||
</Reveal>
|
||||
);
|
||||
}
|
||||
return <Text content={body} tags={ev.tags} creator={ev.pubkey} />;
|
||||
}, [ev, contentWarningAccepted]);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (entry && inView && extendable === false) {
|
||||
|
25
packages/app/src/Element/Reveal.tsx
Normal file
25
packages/app/src/Element/Reveal.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { useState } from "react";
|
||||
|
||||
interface RevealProps {
|
||||
message: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Reveal(props: RevealProps): JSX.Element {
|
||||
const [reveal, setReveal] = useState(false);
|
||||
|
||||
if (!reveal) {
|
||||
return (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setReveal(true);
|
||||
}}
|
||||
className="note-invoice">
|
||||
{props.message}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
}
|
33
packages/app/src/Element/RevealMedia.tsx
Normal file
33
packages/app/src/Element/RevealMedia.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import MediaLink from "Element/MediaLink";
|
||||
import Reveal from "Element/Reveal";
|
||||
import { RootState } from "State/Store";
|
||||
|
||||
interface RevealMediaProps {
|
||||
creator: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export default function RevealMedia(props: RevealMediaProps) {
|
||||
const pref = useSelector((s: RootState) => s.login.preferences);
|
||||
const follows = useSelector((s: RootState) => s.login.follows);
|
||||
const publicKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
|
||||
const hideNonFollows = pref.autoLoadMedia === "follows-only" && !follows.includes(props.creator);
|
||||
const isMine = props.creator === publicKey;
|
||||
const hideMedia = pref.autoLoadMedia === "none" || (!isMine && hideNonFollows);
|
||||
const hostname = new URL(props.link).hostname;
|
||||
|
||||
if (hideMedia) {
|
||||
return (
|
||||
<Reveal
|
||||
message={<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: hostname }} />}>
|
||||
<MediaLink link={props.link} />
|
||||
</Reveal>
|
||||
);
|
||||
} else {
|
||||
return <MediaLink link={props.link} />;
|
||||
}
|
||||
}
|
@ -1,18 +1,17 @@
|
||||
import "./Text.css";
|
||||
import { useMemo, useCallback } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { visit, SKIP } from "unist-util-visit";
|
||||
import * as unist from "unist";
|
||||
import { HexKey, NostrPrefix } from "@snort/nostr";
|
||||
|
||||
import { MentionRegex, InvoiceRegex, HashtagRegex, MagnetRegex } from "Const";
|
||||
import { eventLink, hexToBech32, magnetURIDecode, splitByUrl, unwrap } from "Util";
|
||||
import { MentionRegex, InvoiceRegex, HashtagRegex } from "Const";
|
||||
import { eventLink, hexToBech32, splitByUrl, unwrap } from "Util";
|
||||
import Invoice from "Element/Invoice";
|
||||
import Hashtag from "Element/Hashtag";
|
||||
import Mention from "Element/Mention";
|
||||
import HyperText from "Element/HyperText";
|
||||
import MagnetLink from "Element/MagnetLink";
|
||||
|
||||
export type Fragment = string | React.ReactNode;
|
||||
|
||||
@ -36,7 +35,7 @@ export default function Text({ content, tags, creator, disableMedia }: TextProps
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return splitByUrl(f).map(a => {
|
||||
if (a.match(/^(?:https?|(?:web\+)?nostr):/i)) {
|
||||
if (a.match(/^(?:https?|(?:web\+)?nostr|magnet):/i)) {
|
||||
if (disableMedia ?? false) {
|
||||
return (
|
||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
@ -44,26 +43,7 @@ export default function Text({ content, tags, creator, disableMedia }: TextProps
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <HyperText key={a} link={a} creator={creator} />;
|
||||
}
|
||||
return a;
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractMagnetLinks(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(MagnetRegex).map(a => {
|
||||
if (a.startsWith("magnet:")) {
|
||||
const parsed = magnetURIDecode(a);
|
||||
if (parsed) {
|
||||
return <MagnetLink magnet={parsed} />;
|
||||
}
|
||||
return <HyperText link={a} creator={creator} />;
|
||||
}
|
||||
return a;
|
||||
});
|
||||
@ -168,48 +148,45 @@ export default function Text({ content, tags, creator, disableMedia }: TextProps
|
||||
fragments = extractLinks(fragments);
|
||||
fragments = extractInvoices(fragments);
|
||||
fragments = extractHashtags(fragments);
|
||||
fragments = extractMagnetLinks(fragments);
|
||||
return fragments;
|
||||
}
|
||||
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
p: (x: { children?: React.ReactNode[] }) => transformParagraph({ body: x.children ?? [], tags }),
|
||||
a: (x: { href?: string }) => <HyperText link={x.href ?? ""} creator={creator} />,
|
||||
li: (x: { children?: Fragment[] }) => transformLi({ body: x.children ?? [], tags }),
|
||||
};
|
||||
}, [content]);
|
||||
const components = {
|
||||
p: (x: { children?: React.ReactNode[] }) => transformParagraph({ body: x.children ?? [], tags }),
|
||||
a: (x: { href?: string }) => <HyperText link={x.href ?? ""} creator={creator} />,
|
||||
li: (x: { children?: Fragment[] }) => transformLi({ body: x.children ?? [], tags }),
|
||||
};
|
||||
|
||||
interface Node extends unist.Node<unist.Data> {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const disableMarkdownLinks = useCallback(
|
||||
() => (tree: Node) => {
|
||||
visit(tree, (node, index, parent) => {
|
||||
if (
|
||||
parent &&
|
||||
typeof index === "number" &&
|
||||
(node.type === "link" ||
|
||||
node.type === "linkReference" ||
|
||||
node.type === "image" ||
|
||||
node.type === "imageReference" ||
|
||||
node.type === "definition")
|
||||
) {
|
||||
node.type = "text";
|
||||
const position = unwrap(node.position);
|
||||
node.value = content.slice(position.start.offset, position.end.offset).replace(/\)$/, " )");
|
||||
return SKIP;
|
||||
}
|
||||
});
|
||||
},
|
||||
[content]
|
||||
);
|
||||
return (
|
||||
<div dir="auto">
|
||||
const disableMarkdownLinks = () => (tree: Node) => {
|
||||
visit(tree, (node, index, parent) => {
|
||||
if (
|
||||
parent &&
|
||||
typeof index === "number" &&
|
||||
(node.type === "link" ||
|
||||
node.type === "linkReference" ||
|
||||
node.type === "image" ||
|
||||
node.type === "imageReference" ||
|
||||
node.type === "definition")
|
||||
) {
|
||||
node.type = "text";
|
||||
const position = unwrap(node.position);
|
||||
node.value = content.slice(position.start.offset, position.end.offset).replace(/\)$/, " )");
|
||||
return SKIP;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const element = useMemo(() => {
|
||||
return (
|
||||
<ReactMarkdown className="text" components={components} remarkPlugins={[disableMarkdownLinks]}>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}, [content]);
|
||||
|
||||
return <div dir="auto">{element}</div>;
|
||||
}
|
||||
|
@ -285,7 +285,7 @@ export function groupByPubkey(acc: Record<HexKey, MetadataCache>, user: Metadata
|
||||
|
||||
export function splitByUrl(str: string) {
|
||||
const urlRegex =
|
||||
/((?:http|ftp|https|nostr|web\+nostr):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~_|]))/i;
|
||||
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~_|]))/i;
|
||||
|
||||
return str.split(urlRegex);
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
--font-secondary-color: #a7a7a7;
|
||||
--font-tertiary-color: #a3a3a3;
|
||||
--border-color: rgba(163, 163, 163, 0.3);
|
||||
--font-size: 16px;
|
||||
--font-size-small: 14px;
|
||||
--font-size-tiny: 12px;
|
||||
--font-size: 15px;
|
||||
--font-size-small: 13px;
|
||||
--font-size-tiny: 11px;
|
||||
--modal-bg-color: rgba(0, 0, 0, 0.8);
|
||||
--note-bg: #0c0c0c;
|
||||
--highlight: #8b5cf6;
|
||||
|
Loading…
x
Reference in New Issue
Block a user