blur images that are not from followers

This commit is contained in:
hzrd149 2023-02-07 17:04:18 -06:00
parent e490b6257c
commit af325b6ec0
5 changed files with 102 additions and 874 deletions

View File

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

View File

@ -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"
},

View File

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

View File

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

785
yarn.lock

File diff suppressed because it is too large Load Diff