mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-13 06:09:42 +02:00
show all images in lightbox
This commit is contained in:
parent
018c917b4b
commit
07f67ccc5a
5
.changeset/lovely-plants-leave.md
Normal file
5
.changeset/lovely-plants-leave.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Show all images in lightbox
|
@ -1,10 +1,33 @@
|
||||
import { Box, Image, ImageProps, Link, SimpleGrid, useDisclosure } from "@chakra-ui/react";
|
||||
import { useRef } from "react";
|
||||
import { Box, Image, ImageProps, Link, LinkProps, SimpleGrid, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import appSettings from "../../services/settings/app-settings";
|
||||
import { ImageGalleryLink } from "../image-gallery";
|
||||
import { useTrusted } from "../../providers/trust";
|
||||
import OpenGraphCard from "../open-graph-card";
|
||||
import { EmbedableContent, defaultGetLocation } from "../../helpers/embeds";
|
||||
import { matchLink } from "../../helpers/regexp";
|
||||
import { useRegisterSlide } from "../lightbox-provider";
|
||||
|
||||
export const ImageGalleryLink = ({ children, href, ...props }: Omit<LinkProps, "onClick">) => {
|
||||
const ref = useRef<HTMLAnchorElement | null>(null);
|
||||
const { show } = useRegisterSlide(ref, href ? { type: "image", src: href } : undefined);
|
||||
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
if (href) {
|
||||
e.preventDefault();
|
||||
show();
|
||||
}
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const BlurredImage = (props: ImageProps) => {
|
||||
const { isOpen, onOpen } = useDisclosure();
|
||||
|
@ -1,65 +0,0 @@
|
||||
import { PropsWithChildren, createContext, forwardRef, useCallback, useContext, useMemo, useState } from "react";
|
||||
import { LinkProps, Link, useDisclosure } from "@chakra-ui/react";
|
||||
import Lightbox, { SlideImage } from "yet-another-react-lightbox";
|
||||
import Zoom from "yet-another-react-lightbox/plugins/zoom";
|
||||
import Counter from "yet-another-react-lightbox/plugins/counter";
|
||||
import Download from "yet-another-react-lightbox/plugins/download";
|
||||
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import "yet-another-react-lightbox/plugins/counter.css";
|
||||
|
||||
const GalleryContext = createContext({
|
||||
isOpen: false,
|
||||
openImage(url: string) {},
|
||||
});
|
||||
export function useGalleryContext() {
|
||||
return useContext(GalleryContext);
|
||||
}
|
||||
|
||||
export const ImageGalleryLink = forwardRef(({ children, href, ...props }: Omit<LinkProps, "onClick">, ref) => {
|
||||
const { openImage } = useGalleryContext();
|
||||
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
if (href) {
|
||||
e.preventDefault();
|
||||
openImage(href);
|
||||
}
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
export function ImageGalleryProvider({ children }: PropsWithChildren) {
|
||||
const open = useDisclosure();
|
||||
const [slides, setSlides] = useState<SlideImage[]>([]);
|
||||
|
||||
const openImage = useCallback(
|
||||
(url: string) => {
|
||||
setSlides([{ src: url }]);
|
||||
open.onOpen();
|
||||
},
|
||||
[setSlides, open.onOpen]
|
||||
);
|
||||
|
||||
const context = useMemo(() => ({ isOpen: open.isOpen, openImage }), [open.isOpen, openImage]);
|
||||
|
||||
return (
|
||||
<GalleryContext.Provider value={context}>
|
||||
{children}
|
||||
<Lightbox
|
||||
open={open.isOpen}
|
||||
slides={slides}
|
||||
close={open.onClose}
|
||||
plugins={[Zoom, Counter, Download]}
|
||||
zoom={{ scrollToZoom: true, maxZoomPixelRatio: 4, wheelZoomDistanceFactor: 100 }}
|
||||
/>
|
||||
</GalleryContext.Provider>
|
||||
);
|
||||
}
|
162
src/components/lightbox-provider.tsx
Normal file
162
src/components/lightbox-provider.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import {
|
||||
DependencyList,
|
||||
MutableRefObject,
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { useUnmount } from "react-use";
|
||||
|
||||
import Lightbox, { Slide } from "yet-another-react-lightbox";
|
||||
import Zoom from "yet-another-react-lightbox/plugins/zoom";
|
||||
import Counter from "yet-another-react-lightbox/plugins/counter";
|
||||
import Download from "yet-another-react-lightbox/plugins/download";
|
||||
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import "yet-another-react-lightbox/plugins/counter.css";
|
||||
|
||||
type RefType = MutableRefObject<HTMLElement | null>;
|
||||
|
||||
function getElementPath(element: HTMLElement): HTMLElement[] {
|
||||
if (!element.parentElement) return [element];
|
||||
return [...getElementPath(element.parentElement), element];
|
||||
}
|
||||
function comparePaths(a: HTMLElement[] | null, b: HTMLElement[] | null) {
|
||||
if (a && !b) return -1;
|
||||
if (!b && a) return 1;
|
||||
if (!a || !b) return 0;
|
||||
|
||||
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
||||
if (a[i] !== b[i] && a[i].parentElement === b[i].parentElement) {
|
||||
const parent = a[i].parentElement;
|
||||
if (!parent) return 0;
|
||||
const children = Array.from(parent.children);
|
||||
return Math.sign(children.indexOf(a[i]) - children.indexOf(b[i]));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const LightboxContext = createContext({
|
||||
isOpen: false,
|
||||
removeSlide(ref: RefType) {},
|
||||
showSlide(ref: RefType) {},
|
||||
addSlide(ref: RefType, slide: Slide) {},
|
||||
});
|
||||
export function useLightbox() {
|
||||
return useContext(LightboxContext);
|
||||
}
|
||||
|
||||
export function useRegisterSlide(ref?: RefType, slide?: Slide, watch: DependencyList[] = []) {
|
||||
const { showSlide, addSlide, removeSlide } = useLightbox();
|
||||
const show = useCallback(() => {
|
||||
if (ref) showSlide(ref);
|
||||
}, [ref, showSlide]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref && slide) addSlide(ref, slide);
|
||||
}, [ref, ...watch]);
|
||||
|
||||
useUnmount(() => {
|
||||
if (ref) removeSlide(ref);
|
||||
});
|
||||
|
||||
return { show };
|
||||
}
|
||||
|
||||
type DynamicSlide = {
|
||||
ref: RefType;
|
||||
slide: Slide;
|
||||
};
|
||||
|
||||
const refPaths = new WeakMap<RefType, HTMLElement[]>();
|
||||
function getRefPath(ref: RefType) {
|
||||
if (ref.current === null) return null;
|
||||
const cache = refPaths.get(ref);
|
||||
if (cache) return cache;
|
||||
const path = getElementPath(ref.current);
|
||||
refPaths.set(ref, path);
|
||||
return path;
|
||||
}
|
||||
|
||||
export function LightboxProvider({ children }: PropsWithChildren) {
|
||||
const lightbox = useDisclosure();
|
||||
const [index, setIndex] = useState(0);
|
||||
const [slides, setSlides] = useState<DynamicSlide[]>([]);
|
||||
|
||||
const orderedSlides = useRef<DynamicSlide[]>([]);
|
||||
orderedSlides.current = Array.from(Object.values(slides)).sort((a, b) =>
|
||||
comparePaths(getRefPath(a.ref), getRefPath(b.ref))
|
||||
);
|
||||
|
||||
const addSlide = useCallback(
|
||||
(ref: RefType, slide: Slide) => {
|
||||
setSlides((arr) => {
|
||||
if (arr.some((s) => s.ref === ref)) {
|
||||
return arr.map((s) => (s.ref === ref ? { ref, slide } : s));
|
||||
}
|
||||
return arr.concat({ ref, slide });
|
||||
});
|
||||
},
|
||||
[setSlides]
|
||||
);
|
||||
const removeSlide = useCallback(
|
||||
(ref: RefType) => {
|
||||
setSlides((arr) => arr.filter((s) => s.ref !== ref));
|
||||
},
|
||||
[setSlides]
|
||||
);
|
||||
const showSlide = useCallback(
|
||||
(ref: RefType) => {
|
||||
for (let i = 0; i < orderedSlides.current.length; i++) {
|
||||
if (orderedSlides.current[i].ref === ref) {
|
||||
// set slide index
|
||||
setIndex(i);
|
||||
lightbox.onOpen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// else select first image
|
||||
setIndex(0);
|
||||
lightbox.onOpen();
|
||||
},
|
||||
[lightbox.onOpen, setIndex]
|
||||
);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({ isOpen: lightbox.isOpen, removeSlide, addSlide, showSlide }),
|
||||
[lightbox.isOpen, removeSlide, addSlide, showSlide]
|
||||
);
|
||||
|
||||
const lightboxSlides = useMemo(() => orderedSlides.current.map((s) => s.slide), [orderedSlides.current, slides]);
|
||||
|
||||
const handleView = useCallback(
|
||||
({ index }: { index: number }) => {
|
||||
setIndex(index);
|
||||
},
|
||||
[setIndex]
|
||||
);
|
||||
|
||||
return (
|
||||
<LightboxContext.Provider value={context}>
|
||||
{children}
|
||||
<Lightbox
|
||||
index={index}
|
||||
open={lightbox.isOpen}
|
||||
slides={lightboxSlides}
|
||||
close={lightbox.onClose}
|
||||
plugins={[Zoom, Counter, Download]}
|
||||
zoom={{ scrollToZoom: true, maxZoomPixelRatio: 4, wheelZoomDistanceFactor: 500 }}
|
||||
controller={{ closeOnBackdropClick: true, closeOnPullDown: true }}
|
||||
on={{ view: handleView }}
|
||||
/>
|
||||
</LightboxContext.Provider>
|
||||
);
|
||||
}
|
@ -19,7 +19,7 @@ import {
|
||||
renderOpenGraphUrl,
|
||||
embedImageGallery,
|
||||
} from "../embed-types";
|
||||
import { ImageGalleryProvider } from "../image-gallery";
|
||||
import { LightboxProvider } from "../lightbox-provider";
|
||||
import { renderRedditUrl } from "../embed-types/reddit";
|
||||
|
||||
function buildContents(event: NostrEvent | DraftNostrEvent) {
|
||||
@ -62,10 +62,10 @@ export const NoteContents = React.memo(({ event, ...props }: NoteContentsProps &
|
||||
const content = buildContents(event);
|
||||
|
||||
return (
|
||||
<ImageGalleryProvider>
|
||||
<LightboxProvider>
|
||||
<Box whiteSpace="pre-wrap" {...props}>
|
||||
{content}
|
||||
</Box>
|
||||
</ImageGalleryProvider>
|
||||
</LightboxProvider>
|
||||
);
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import { useCallback } from "react";
|
||||
import { Flex, FlexProps, SimpleGrid } from "@chakra-ui/react";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import GenericNoteTimeline from "./generic-note-timeline";
|
||||
import { ImageGalleryProvider } from "../image-gallery";
|
||||
import { LightboxProvider } from "../lightbox-provider";
|
||||
import MediaTimeline from "./media-timeline";
|
||||
import { TimelineLoader } from "../../classes/timeline-loader";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
@ -44,11 +44,11 @@ export default function TimelinePage({
|
||||
|
||||
case "images":
|
||||
return (
|
||||
<ImageGalleryProvider>
|
||||
<LightboxProvider>
|
||||
<SimpleGrid columns={[1, 2, 2, 3, 4, 5]} gap="4">
|
||||
<MediaTimeline timeline={timeline} />
|
||||
</SimpleGrid>
|
||||
</ImageGalleryProvider>
|
||||
</LightboxProvider>
|
||||
);
|
||||
|
||||
case "health":
|
||||
|
@ -2,8 +2,8 @@ import React, { useMemo, useRef } from "react";
|
||||
import { TimelineLoader } from "../../../classes/timeline-loader";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import { matchImageUrls } from "../../../helpers/regexp";
|
||||
import { ImageGalleryLink, ImageGalleryProvider } from "../../image-gallery";
|
||||
import { Box, IconButton } from "@chakra-ui/react";
|
||||
import { LightboxProvider, useRegisterSlide } from "../../lightbox-provider";
|
||||
import { Box, IconButton, Link } from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
import { getSharableNoteId } from "../../../helpers/nip19";
|
||||
@ -19,9 +19,26 @@ const ImagePreview = React.memo(({ image }: { image: ImagePreview }) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, image.eventId);
|
||||
|
||||
const { show } = useRegisterSlide(ref, { type: "image", src: image.src });
|
||||
|
||||
return (
|
||||
<ImageGalleryLink href={image.src} position="relative" ref={ref}>
|
||||
<Box aspectRatio={1} backgroundImage={`url(${image.src})`} backgroundSize="cover" backgroundPosition="center" />
|
||||
<Link
|
||||
href={image.src}
|
||||
position="relative"
|
||||
onClick={(e) => {
|
||||
if (image.src) {
|
||||
e.preventDefault();
|
||||
show();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
aspectRatio={1}
|
||||
backgroundImage={`url(${image.src})`}
|
||||
backgroundSize="cover"
|
||||
backgroundPosition="center"
|
||||
ref={ref}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<ExternalLinkIcon />}
|
||||
aria-label="Open note"
|
||||
@ -36,7 +53,7 @@ const ImagePreview = React.memo(({ image }: { image: ImagePreview }) => {
|
||||
navigate(`/n/${getSharableNoteId(image.eventId)}`);
|
||||
}}
|
||||
/>
|
||||
</ImageGalleryLink>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
@ -59,10 +76,10 @@ export default function MediaTimeline({ timeline }: { timeline: TimelineLoader }
|
||||
}, [events]);
|
||||
|
||||
return (
|
||||
<ImageGalleryProvider>
|
||||
<LightboxProvider>
|
||||
{images.map((image) => (
|
||||
<ImagePreview key={image.eventId + "-" + image.index} image={image} />
|
||||
))}
|
||||
</ImageGalleryProvider>
|
||||
</LightboxProvider>
|
||||
);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import ZapModal from "../../../../components/zap-modal";
|
||||
import { LightningIcon } from "../../../../components/icons";
|
||||
import ChatMessage from "./chat-message";
|
||||
import ZapMessage from "./zap-message";
|
||||
import { ImageGalleryProvider } from "../../../../components/image-gallery";
|
||||
import { LightboxProvider } from "../../../../components/lightbox-provider";
|
||||
import IntersectionObserverProvider from "../../../../providers/intersection-observer";
|
||||
import useUserLNURLMetadata from "../../../../hooks/use-user-lnurl-metadata";
|
||||
import { useInvoiceModalContext } from "../../../../providers/invoice-modal";
|
||||
@ -126,7 +126,7 @@ export default function StreamChat({
|
||||
return (
|
||||
<>
|
||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
||||
<ImageGalleryProvider>
|
||||
<LightboxProvider>
|
||||
<Card {...props} overflow="hidden" background={isChatLog ? "transparent" : undefined}>
|
||||
{!isPopup && (
|
||||
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
|
||||
@ -184,7 +184,7 @@ export default function StreamChat({
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</ImageGalleryProvider>
|
||||
</LightboxProvider>
|
||||
</IntersectionObserverProvider>
|
||||
{zapModal.isOpen && (
|
||||
<ZapModal
|
||||
|
Loading…
x
Reference in New Issue
Block a user