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