mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-12 13:49:33 +02:00
blur images that are not from followers
This commit is contained in:
parent
e490b6257c
commit
af325b6ec0
@ -4,20 +4,21 @@
|
||||
|
||||
- [x] Home feed
|
||||
- [x] Discovery Feed
|
||||
- [x] Dark theme
|
||||
- [x] Preview twitter / youtube links
|
||||
- [x] Lighting invoices
|
||||
- [ ] Profile management
|
||||
- [ ] Make post
|
||||
- [ ] Relay management
|
||||
- [x] Dark theme
|
||||
- [ ] NIP-05 support
|
||||
- [x] Render markdown
|
||||
- [x] Preview twitter / youtube links
|
||||
- [ ] Broadcast event
|
||||
- [ ] Manage followers
|
||||
- [x] Lighting invoices
|
||||
- [ ] Image upload
|
||||
- [ ] Thread view
|
||||
- [ ] User tipping
|
||||
- [ ] Reactions
|
||||
- [ ] Blurred or hidden images and embeds for people you dont follow
|
||||
- [ ] "Read more" Button for long posts
|
||||
|
||||
## Setup
|
||||
|
||||
|
@ -24,16 +24,9 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.41.2",
|
||||
"react-markdown": "^8.0.4",
|
||||
"react-router-dom": "^6.5.0",
|
||||
"react-singleton-hook": "^4.0.1",
|
||||
"react-use": "^17.4.0",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"rehype-truncate": "^1.2.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-images": "^3.1.0",
|
||||
"remark-linkify-regex": "^1.2.1",
|
||||
"remark-unwrap-images": "^3.0.1",
|
||||
"rxjs": "^7.8.0",
|
||||
"webln": "^0.3.2"
|
||||
},
|
||||
|
@ -24,6 +24,9 @@ import { PostContents } from "./post-contents";
|
||||
import { PostMenu } from "./post-menu";
|
||||
import { PostCC } from "./post-cc";
|
||||
import { isReply } from "../../helpers/nostr-event";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import identity from "../../services/identity";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
|
||||
export type PostProps = {
|
||||
event: NostrEvent;
|
||||
@ -32,6 +35,10 @@ export const Post = React.memo(({ event }: PostProps) => {
|
||||
const navigate = useNavigate();
|
||||
const metadata = useUserMetadata(event.pubkey);
|
||||
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const contacts = useUserContacts(pubkey);
|
||||
const following = contacts?.contacts || [];
|
||||
|
||||
return (
|
||||
<Card padding="2" variant="outline">
|
||||
<CardHeader padding="0" mb="2">
|
||||
@ -57,7 +64,7 @@ export const Post = React.memo(({ event }: PostProps) => {
|
||||
<CardBody padding="0" mb="2">
|
||||
<VStack alignItems="flex-start" justifyContent="stretch">
|
||||
<Box overflow="hidden" width="100%">
|
||||
<PostContents content={event.content} />
|
||||
<PostContents content={event.content} trusted={following.includes(event.pubkey)} />
|
||||
</Box>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
|
@ -1,96 +1,100 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AspectRatio,
|
||||
Box,
|
||||
Image,
|
||||
ImageProps,
|
||||
Link,
|
||||
LinkProps,
|
||||
} from "@chakra-ui/react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkImages from "remark-images";
|
||||
import remarkUnwrapImages from "remark-unwrap-images";
|
||||
import rehypeExternalLinks from "rehype-external-links";
|
||||
// @ts-ignore
|
||||
import linkifyRegex from "remark-linkify-regex";
|
||||
import { AspectRatio, Box, Image, ImageProps, Link, useDisclosure } from "@chakra-ui/react";
|
||||
import { InlineInvoiceCard } from "../inline-invoice-card";
|
||||
import { TweetEmbed } from "../tweet-embed";
|
||||
|
||||
const lightningInvoiceRegExp = /(lightning:)?LNBC[A-Za-z0-9]+/i;
|
||||
|
||||
// copied from https://stackoverflow.com/a/37704433
|
||||
const youtubeVideoLink =
|
||||
/^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/i;
|
||||
|
||||
const twitterLink =
|
||||
/https?:\/\/twitter\.com\/(?:\#!\/)?(\w+)\/status(es)?\/(\d+)/i;
|
||||
|
||||
const CustomLink = (props: LinkProps) => <Link color="blue.500" {...props} />;
|
||||
const CustomImage = (props: ImageProps) => (
|
||||
<Image {...props} maxWidth="30rem" />
|
||||
);
|
||||
|
||||
const HandleLinkTypes = (props: LinkProps) => {
|
||||
let href = props.href;
|
||||
// @ts-ignore
|
||||
if (href === "javascript:void(0)") href = String(props.children[0]);
|
||||
|
||||
if (href) {
|
||||
if (lightningInvoiceRegExp.test(href)) {
|
||||
return (
|
||||
<InlineInvoiceCard paymentRequest={href.replace(/lightning:/i, "")} />
|
||||
);
|
||||
}
|
||||
if (youtubeVideoLink.test(href)) {
|
||||
const parts = youtubeVideoLink.exec(href);
|
||||
|
||||
return parts ? (
|
||||
<AspectRatio ratio={16 / 10} maxWidth="30rem">
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${parts[6]}`}
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
width="100%"
|
||||
></iframe>
|
||||
</AspectRatio>
|
||||
) : (
|
||||
<CustomLink {...props} />
|
||||
);
|
||||
}
|
||||
if (twitterLink.test(href)) {
|
||||
return <TweetEmbed href={href} conversation={false} />;
|
||||
}
|
||||
}
|
||||
return <CustomLink {...props} />;
|
||||
const BlurredImage = (props: ImageProps) => {
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
return <Image onClick={onToggle} cursor="pointer" filter={isOpen ? "" : "blur(1.5rem)"} {...props} />;
|
||||
};
|
||||
|
||||
const components = {
|
||||
img: CustomImage,
|
||||
a: HandleLinkTypes,
|
||||
};
|
||||
const embeds: { regexp: RegExp; render: (match: RegExpMatchArray, trusted: boolean) => JSX.Element | string }[] = [
|
||||
// Lightning Invoice
|
||||
{
|
||||
regexp: /(lightning:)?(LNBC[A-Za-z0-9]+)/im,
|
||||
render: (match) => <InlineInvoiceCard key={match[0]} paymentRequest={match[2]} />,
|
||||
},
|
||||
// Twitter tweet
|
||||
{
|
||||
regexp: /^https?:\/\/twitter\.com\/(?:\#!\/)?(\w+)\/status(es)?\/(\d+)/im,
|
||||
render: (match) => <TweetEmbed key={match[0]} href={match[0]} conversation={false} />,
|
||||
},
|
||||
// Youtube Video
|
||||
{
|
||||
regexp:
|
||||
/^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/im,
|
||||
render: (match) => (
|
||||
<AspectRatio ratio={16 / 10} maxWidth="30rem">
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${match[6]}`}
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
width="100%"
|
||||
></iframe>
|
||||
</AspectRatio>
|
||||
),
|
||||
},
|
||||
// Image
|
||||
{
|
||||
regexp: /(https?:\/\/)([\da-z\.-]+\.[a-z\.]{2,6})([\/\w\.-]+\.(svg|gif|png|jpg|jpeg|webp|avif))/im,
|
||||
render: (match, trusted) => {
|
||||
const ImageComponent = trusted ? Image : BlurredImage;
|
||||
return <ImageComponent key={match[0]} src={match[0]} maxWidth="30rem" />;
|
||||
},
|
||||
},
|
||||
// Video
|
||||
{
|
||||
regexp: /(https?:\/\/)([\da-z\.-]+\.[a-z\.]{2,6})([\/\w\.-]+\.(mp4))/im,
|
||||
render: (match) => (
|
||||
<AspectRatio ratio={16 / 9} maxWidth="30rem">
|
||||
<video key={match[0]} src={match[0]} controls />
|
||||
</AspectRatio>
|
||||
),
|
||||
},
|
||||
// Link
|
||||
{
|
||||
regexp: /(https?:\/\/[^\s]+)/im,
|
||||
render: (match) => (
|
||||
<Link key={match[0]} color="blue.500" href={match[0]} target="_blank">
|
||||
{match[0]}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export type PostContentsProps = {
|
||||
content: string;
|
||||
trusted?: boolean;
|
||||
};
|
||||
|
||||
export const PostContents = React.memo(({ content }: PostContentsProps) => {
|
||||
const fixedLines = content.replace(/(?<! )\n/g, " \n");
|
||||
export const PostContents = React.memo(({ content, trusted }: PostContentsProps) => {
|
||||
const parts: (string | JSX.Element)[] = [content];
|
||||
|
||||
for (const { regexp, render } of embeds) {
|
||||
let i = 0;
|
||||
while (i < 1000) {
|
||||
i++;
|
||||
const str = parts.pop();
|
||||
if (typeof str !== "string" || str.length === 0) {
|
||||
str && parts.push(str);
|
||||
break;
|
||||
}
|
||||
|
||||
const match = str.match(regexp);
|
||||
if (match && match.index !== undefined) {
|
||||
parts.push(str.slice(0, match.index));
|
||||
parts.push(render(match, trusted ?? false));
|
||||
parts.push(str.slice(match.index + match[0].length, str.length));
|
||||
} else {
|
||||
parts.push(str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[
|
||||
remarkImages,
|
||||
remarkUnwrapImages,
|
||||
remarkGfm,
|
||||
linkifyRegex(lightningInvoiceRegExp),
|
||||
]}
|
||||
rehypePlugins={[[rehypeExternalLinks, { target: "_blank" }]]}
|
||||
components={components}
|
||||
>
|
||||
{fixedLines}
|
||||
</ReactMarkdown>
|
||||
<Box whiteSpace="pre-wrap">{parts.map((part) => (typeof part === "string" ? <span>{part}</span> : part))}</Box>
|
||||
);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user