mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-28 20:43:33 +02:00
Merge branch 'next'
This commit is contained in:
5
.changeset/eight-dryers-drive.md
Normal file
5
.changeset/eight-dryers-drive.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Make photo flush with edge of note
|
5
.changeset/rich-plants-explode.md
Normal file
5
.changeset/rich-plants-explode.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add content warning for NIP-36 notes
|
5
.changeset/rude-beds-attack.md
Normal file
5
.changeset/rude-beds-attack.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Replace laggy photo lightbox
|
5
.changeset/tidy-trains-tap.md
Normal file
5
.changeset/tidy-trains-tap.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Fix subscription id too long
|
@@ -19,7 +19,6 @@
|
||||
"idb": "^7.1.1",
|
||||
"identicon.js": "^2.3.3",
|
||||
"light-bolt11-decoder": "^2.1.0",
|
||||
"lightgallery": "^2.7.1",
|
||||
"moment": "^2.29.4",
|
||||
"noble-secp256k1": "^1.2.14",
|
||||
"nostr-tools": "^1.8.3",
|
||||
|
@@ -1,10 +1,7 @@
|
||||
import { Box, Image, ImageProps, Link, useDisclosure } from "@chakra-ui/react";
|
||||
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
|
||||
import appSettings from "../../services/app-settings";
|
||||
|
||||
import LightGallery from "lightgallery/react";
|
||||
import lgThumbnail from "lightgallery/plugins/thumbnail";
|
||||
import lgZoom from "lightgallery/plugins/zoom";
|
||||
import { ImageGalleryLink } from "../image-gallery";
|
||||
|
||||
const BlurredImage = (props: ImageProps) => {
|
||||
const { isOpen, onOpen } = useDisclosure();
|
||||
@@ -28,11 +25,9 @@ export function embedImages(content: EmbedableContent, trusted = false) {
|
||||
const src = match[0];
|
||||
|
||||
return (
|
||||
<LightGallery plugins={[lgThumbnail, lgZoom]} licenseKey="1234-5678-9101-1121">
|
||||
<Link href={src} target="_blank" display="inline-block">
|
||||
<ImageComponent src={thumbnail} cursor="pointer" maxW="30rem" />
|
||||
</Link>
|
||||
</LightGallery>
|
||||
<ImageGalleryLink href={src} target="_blank" display="block" mx="-2">
|
||||
<ImageComponent src={thumbnail} cursor="pointer" maxW="30rem" w="full" />
|
||||
</ImageGalleryLink>
|
||||
);
|
||||
},
|
||||
name: "Image",
|
||||
|
76
src/components/image-gallery.tsx
Normal file
76
src/components/image-gallery.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { DownloadIcon } from "@chakra-ui/icons";
|
||||
import {
|
||||
LinkProps,
|
||||
Link,
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Image,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from "@chakra-ui/react";
|
||||
import { PropsWithChildren, createContext, useContext, useState } from "react";
|
||||
|
||||
const GalleryContext = createContext({
|
||||
isOpen: false,
|
||||
openImage(url: string) {},
|
||||
});
|
||||
export function useGalleryContext() {
|
||||
return useContext(GalleryContext);
|
||||
}
|
||||
|
||||
export function ImageGalleryLink({ children, href, ...props }: Omit<LinkProps, "onClick">) {
|
||||
const { openImage } = useGalleryContext();
|
||||
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
if (href) {
|
||||
e.preventDefault();
|
||||
openImage(href);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function ImageGalleryProvider({ children }: PropsWithChildren) {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [image, setImage] = useState("");
|
||||
|
||||
const openImage = (url: string) => {
|
||||
setImage(url);
|
||||
onOpen();
|
||||
};
|
||||
const context = { isOpen, openImage };
|
||||
|
||||
return (
|
||||
<GalleryContext.Provider value={context}>
|
||||
{children}
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="6xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Image</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody p="0">
|
||||
<Image src={image} w="full" />
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button colorScheme="brand" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</GalleryContext.Provider>
|
||||
);
|
||||
}
|
@@ -2,7 +2,12 @@ import React, { useMemo } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import moment from "moment";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
@@ -13,6 +18,7 @@ import {
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Spacer,
|
||||
} from "@chakra-ui/react";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { UserAvatarLink } from "../user-avatar-link";
|
||||
@@ -29,7 +35,7 @@ import { convertTimestampToDate } from "../../helpers/date";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import ReactionButton from "./buttons/reaction-button";
|
||||
import NoteZapButton from "./note-zap-button";
|
||||
import { ExpandProvider } from "./expanded";
|
||||
import { ExpandProvider, useExpand } from "./expanded";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import appSettings from "../../services/app-settings";
|
||||
import EventVerificationIcon from "../event-verification-icon";
|
||||
@@ -38,6 +44,31 @@ import { RepostButton } from "./buttons/repost-button";
|
||||
import { QuoteRepostButton } from "./buttons/quote-repost-button";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { ExternalLinkIcon } from "../icons";
|
||||
import SensitiveContentWarning from "../sensitive-content-warning";
|
||||
import useAppSettings from "../../hooks/use-app-settings";
|
||||
|
||||
function NoteContentWithWarning({ event, maxHeight }: { event: NostrEvent; maxHeight?: number }) {
|
||||
const account = useCurrentAccount();
|
||||
const expand = useExpand();
|
||||
const settings = useAppSettings();
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
const contacts = useUserContacts(account.pubkey, readRelays);
|
||||
const following = contacts?.contacts || [];
|
||||
|
||||
const contentWarning = event.tags.find((t) => t[0] === "content-warning")?.[1];
|
||||
const showContentWarning = settings.showContentWarning && contentWarning && !expand?.expanded;
|
||||
|
||||
return showContentWarning ? (
|
||||
<SensitiveContentWarning description={contentWarning} />
|
||||
) : (
|
||||
<NoteContents
|
||||
event={event}
|
||||
trusted={event.pubkey === account.pubkey || following.includes(event.pubkey)}
|
||||
maxHeight={maxHeight}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type NoteProps = {
|
||||
event: NostrEvent;
|
||||
@@ -46,13 +77,8 @@ export type NoteProps = {
|
||||
};
|
||||
export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const account = useCurrentAccount();
|
||||
const { showReactions, showSignatureVerification } = useSubject(appSettings);
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
const contacts = useUserContacts(account.pubkey, readRelays);
|
||||
const following = contacts?.contacts || [];
|
||||
|
||||
// find mostr external link
|
||||
const externalLink = useMemo(() => event.tags.find((t) => t[0] === "mostr"), [event]);
|
||||
|
||||
@@ -74,12 +100,8 @@ export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteP
|
||||
</Link>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<CardBody px="2" py="0">
|
||||
<NoteContents
|
||||
event={event}
|
||||
trusted={event.pubkey === account.pubkey || following.includes(event.pubkey)}
|
||||
maxHeight={maxHeight}
|
||||
/>
|
||||
<CardBody p="0">
|
||||
<NoteContentWithWarning event={event} maxHeight={maxHeight} />
|
||||
</CardBody>
|
||||
<CardFooter padding="2" display="flex" gap="2">
|
||||
<ButtonGroup size="sm" variant="link">
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { Box, Text } from "@chakra-ui/react";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
import styled from "@emotion/styled";
|
||||
import { useExpand } from "./expanded";
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
embedAppleMusic,
|
||||
embedNostrHashtags,
|
||||
} from "../embed-types";
|
||||
import { ImageGalleryProvider } from "../image-gallery";
|
||||
|
||||
function buildContents(event: NostrEvent | DraftNostrEvent, trusted: boolean = false) {
|
||||
let content: EmbedableContent = [event.content];
|
||||
@@ -82,19 +83,28 @@ export const NoteContents = React.memo(({ event, trusted, maxHeight }: NoteConte
|
||||
const showOverlay = !!maxHeight && !expand?.expanded && innerHeight > maxHeight;
|
||||
|
||||
return (
|
||||
<Box
|
||||
whiteSpace="pre-wrap"
|
||||
maxHeight={!expand?.expanded ? maxHeight : undefined}
|
||||
position="relative"
|
||||
overflow={maxHeight && !expand?.expanded ? "hidden" : "initial"}
|
||||
onLoad={() => testHeight()}
|
||||
>
|
||||
<div ref={ref}>
|
||||
{content.map((part, i) => (
|
||||
<span key={"part-" + i}>{part}</span>
|
||||
))}
|
||||
</div>
|
||||
{showOverlay && <GradientOverlay onClick={expand?.onExpand} />}
|
||||
</Box>
|
||||
<ImageGalleryProvider>
|
||||
<Box
|
||||
whiteSpace="pre-wrap"
|
||||
maxHeight={!expand?.expanded ? maxHeight : undefined}
|
||||
position="relative"
|
||||
overflow={maxHeight && !expand?.expanded ? "hidden" : "initial"}
|
||||
onLoad={() => testHeight()}
|
||||
px="2"
|
||||
>
|
||||
<div ref={ref}>
|
||||
{content.map((part, i) =>
|
||||
typeof part === "string" ? (
|
||||
<Text as="span" key={"part-" + i}>
|
||||
{part}
|
||||
</Text>
|
||||
) : (
|
||||
React.cloneElement(part, { key: "part-" + i })
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{showOverlay && <GradientOverlay onClick={expand?.onExpand} />}
|
||||
</Box>
|
||||
</ImageGalleryProvider>
|
||||
);
|
||||
});
|
||||
|
42
src/components/sensitive-content-warning.tsx
Normal file
42
src/components/sensitive-content-warning.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Alert, AlertDescription, AlertIcon, AlertProps, AlertTitle, Button, Spacer, useModal } from "@chakra-ui/react";
|
||||
import { useIsMobile } from "../hooks/use-is-mobile";
|
||||
import { useExpand } from "./note/expanded";
|
||||
|
||||
export default function SensitiveContentWarning({ description }: { description: string } & AlertProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const expand = useExpand();
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Alert
|
||||
status="warning"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
textAlign="center"
|
||||
height="200px"
|
||||
>
|
||||
<AlertIcon boxSize="40px" mr={0} />
|
||||
<AlertTitle mt={4} mb={1} fontSize="lg">
|
||||
Sensitive Content
|
||||
</AlertTitle>
|
||||
<AlertDescription maxWidth="sm">{description}</AlertDescription>
|
||||
<Button mt="2" onClick={expand?.onExpand} colorScheme="red">
|
||||
Show
|
||||
</Button>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert status="warning">
|
||||
<AlertIcon boxSize="30px" mr="4" />
|
||||
<AlertTitle fontSize="lg">Sensitive Content</AlertTitle>
|
||||
<AlertDescription maxWidth="sm">{description}</AlertDescription>
|
||||
<Spacer />
|
||||
<Button mt="2" onClick={expand?.onExpand} colorScheme="red">
|
||||
Show
|
||||
</Button>
|
||||
</Alert>
|
||||
);
|
||||
}
|
@@ -3,10 +3,6 @@ import { createRoot } from "react-dom/client";
|
||||
import { App } from "./app";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
import "lightgallery/css/lightgallery.css";
|
||||
import "lightgallery/css/lg-zoom.css";
|
||||
import "lightgallery/css/lg-thumbnail.css";
|
||||
|
||||
// register nostr: protocol handler
|
||||
try {
|
||||
navigator.registerProtocolHandler("web+nostr", new URL("/l/%s", location.origin).toString());
|
||||
|
@@ -26,6 +26,7 @@ export type AppSettings = {
|
||||
zapAmounts: number[];
|
||||
primaryColor: string;
|
||||
imageProxy: string;
|
||||
showContentWarning: boolean;
|
||||
};
|
||||
|
||||
export const defaultSettings: AppSettings = {
|
||||
@@ -39,6 +40,7 @@ export const defaultSettings: AppSettings = {
|
||||
zapAmounts: [50, 200, 500, 1000],
|
||||
primaryColor: "#8DB600",
|
||||
imageProxy: "",
|
||||
showContentWarning: true,
|
||||
};
|
||||
|
||||
function parseAppSettings(event: NostrEvent): AppSettings {
|
||||
|
@@ -3,7 +3,7 @@ import { Button, Flex, Spinner } from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import { Note } from "../../components/note";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { isReply } from "../../helpers/nostr-event";
|
||||
import { isReply, truncatedId } from "../../helpers/nostr-event";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
@@ -69,7 +69,7 @@ export default function DiscoverTab() {
|
||||
const throttledPubkeys = useThrottle(pubkeys, 1000);
|
||||
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
`${account.pubkey}-discover`,
|
||||
`${truncatedId(account.pubkey)}-discover`,
|
||||
relays,
|
||||
{ authors: throttledPubkeys, kinds: [1], since: moment().subtract(1, "hour").unix() },
|
||||
{ pageSize: moment.duration(1, "hour").asSeconds(), enabled: throttledPubkeys.length > 0 }
|
||||
|
@@ -2,7 +2,7 @@ import { Button, Flex, FormControl, FormLabel, Spinner, Switch } from "@chakra-u
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import moment from "moment";
|
||||
import { Note } from "../../components/note";
|
||||
import { isReply } from "../../helpers/nostr-event";
|
||||
import { isReply, truncatedId } from "../../helpers/nostr-event";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
import { AddIcon } from "@chakra-ui/icons";
|
||||
@@ -25,7 +25,7 @@ export default function FollowingTab() {
|
||||
|
||||
const following = contacts?.contacts || [];
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
`${account.pubkey}-following-posts`,
|
||||
`${truncatedId(account.pubkey)}-following-posts`,
|
||||
readRelays,
|
||||
{ authors: following, kinds: [1, 6], since: moment().subtract(2, "hour").unix() },
|
||||
{ pageSize: moment.duration(2, "hour").asSeconds(), enabled: following.length > 0 }
|
||||
|
@@ -44,7 +44,7 @@ function ColorPicker({ value, onPickColor, ...props }: { onPickColor?: (color: s
|
||||
}
|
||||
|
||||
export default function DisplaySettings() {
|
||||
const { blurImages, colorMode, primaryColor, updateSettings } = useAppSettings();
|
||||
const { blurImages, colorMode, primaryColor, updateSettings, showContentWarning } = useAppSettings();
|
||||
|
||||
return (
|
||||
<AccordionItem>
|
||||
@@ -106,6 +106,21 @@ export default function DisplaySettings() {
|
||||
<span>Enabled: blur images for people you aren't following</span>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Flex alignItems="center">
|
||||
<FormLabel htmlFor="show-content-warning" mb="0">
|
||||
Show content warning
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="show-content-warning"
|
||||
isChecked={showContentWarning}
|
||||
onChange={(v) => updateSettings({ showContentWarning: v.target.checked })}
|
||||
/>
|
||||
</Flex>
|
||||
<FormHelperText>
|
||||
<span>Enabled: shows a warning for notes with NIP-36 Content Warning</span>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Flex alignItems="center">
|
||||
<FormLabel htmlFor="show-ads" mb="0">
|
||||
|
Reference in New Issue
Block a user