Render multiple images in note as gallery

This commit is contained in:
hzrd149 2023-08-14 14:38:22 -05:00
parent c6b5df0426
commit 1148093517
5 changed files with 97 additions and 5 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Render multiple images as image gallery

View File

@ -1,8 +1,10 @@
import { Box, Image, ImageProps, Link, useDisclosure } from "@chakra-ui/react";
import { Box, Image, ImageProps, Link, 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";
const BlurredImage = (props: ImageProps) => {
const { isOpen, onOpen } = useDisclosure();
@ -26,13 +28,21 @@ const BlurredImage = (props: ImageProps) => {
);
};
const EmbeddedImage = ({ src }: { src: string }) => {
const EmbeddedImage = ({ src, inGallery }: { src: string; inGallery?: boolean }) => {
const trusted = useTrusted();
const ImageComponent = trusted || !appSettings.value.blurImages ? Image : BlurredImage;
const thumbnail = appSettings.value.imageProxy
? new URL(`/256,fit/${src}`, appSettings.value.imageProxy).toString()
: src;
if (inGallery) {
return (
<ImageGalleryLink href={src} target="_blank">
<ImageComponent src={thumbnail} cursor="pointer" />
</ImageGalleryLink>
);
}
return (
<ImageGalleryLink href={src} target="_blank" display="block" mx="-2">
<ImageComponent src={thumbnail} cursor="pointer" maxH={["initial", "35vh"]} mx={["auto", 0]} />
@ -40,8 +50,79 @@ const EmbeddedImage = ({ src }: { src: string }) => {
);
};
// note1n06jceulg3gukw836ghd94p0ppwaz6u3mksnnz960d8vlcp2fnqsgx3fu9
function ImageGallery({ images }: { images: string[] }) {
return (
<SimpleGrid columns={[2, 2, 3, 3, 4]}>
{images.map((src) => (
<EmbeddedImage key={src} src={src} inGallery />
))}
</SimpleGrid>
);
}
const imageExt = [".svg", ".gif", ".png", ".jpg", ".jpeg", ".webp", ".avif"];
export function embedImageGallery(content: EmbedableContent): EmbedableContent {
return content
.map((subContent, i) => {
if (typeof subContent === "string") {
const matches = Array.from(subContent.matchAll(matchLink));
const newContent: EmbedableContent = [];
let lastBatchEnd = 0;
let batch: RegExpMatchArray[] = [];
const renderBatch = () => {
if (batch.length > 1) {
// render previous batch
const lastMatchPosition = defaultGetLocation(batch[batch.length - 1]);
const before = subContent.substring(lastBatchEnd, defaultGetLocation(batch[0]).start);
const render = <ImageGallery images={batch.map((m) => m[0])} />;
newContent.push(before, render);
lastBatchEnd = lastMatchPosition.end;
}
batch = [];
};
for (const match of matches) {
try {
const url = new URL(match[0]);
if (!imageExt.some((ext) => url.pathname.endsWith(ext))) throw new Error("not an image");
// if this is the first image, add it to the batch
if (batch.length === 0) {
batch = [match];
continue;
}
const last = defaultGetLocation(batch[batch.length - 1]);
const position = defaultGetLocation(match);
const space = subContent.substring(last.end, position.start).trim();
// if there was a non-space between this and the last batch
if (space.length > 0) renderBatch();
batch.push(match);
} catch (e) {
// start a new batch without current match
batch = [];
}
}
renderBatch();
newContent.push(subContent.substring(lastBatchEnd));
return newContent;
}
return subContent;
})
.flat();
}
// note1n06jceulg3gukw836ghd94p0ppwaz6u3mksnnz960d8vlcp2fnqsgx3fu9
export function renderImageUrl(match: URL) {
if (!imageExt.some((ext) => match.pathname.endsWith(ext))) return null;

View File

@ -17,6 +17,7 @@ import {
renderVideoUrl,
embedEmoji,
renderOpenGraphUrl,
embedImageGallery,
} from "../embed-types";
import { ImageGalleryProvider } from "../image-gallery";
import { renderRedditUrl } from "../embed-types/reddit";
@ -24,6 +25,9 @@ import { renderRedditUrl } from "../embed-types/reddit";
function buildContents(event: NostrEvent | DraftNostrEvent) {
let content: EmbedableContent = [event.content.trim()];
// image gallery
content = embedImageGallery(content);
// common
content = embedUrls(content, [
renderYoutubeUrl,

View File

@ -1,4 +1,5 @@
import { cloneElement } from "react";
import { matchLink } from "./regexp";
export type EmbedableContent = (string | JSX.Element)[];
export type EmbedType = {
@ -8,7 +9,7 @@ export type EmbedType = {
getLocation?: (match: RegExpMatchArray) => { start: number; end: number };
};
function defaultGetLocation(match: RegExpMatchArray) {
export function defaultGetLocation(match: RegExpMatchArray) {
if (match.index === undefined) throw new Error("match dose not have index");
return {
start: match.index,
@ -72,7 +73,7 @@ export type LinkEmbedHandler = (link: URL) => JSX.Element | string | null;
export function embedUrls(content: EmbedableContent, handlers: LinkEmbedHandler[]) {
return embedJSX(content, {
name: "embedUrls",
regexp: /https?:\/\/([a-zA-Z0-9\.\-]+\.[a-zA-Z]+)([\p{Letter}\p{Number}&\.-\/\?=#\-@%\+_,:]*)/giu,
regexp: matchLink,
render: (match) => {
try {
const url = new URL(match[0]);

View File

@ -4,3 +4,4 @@ export const matchImageUrls =
export const matchNostrLink = /(nostr:|@)?((npub|note|nprofile|nevent)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58,})/gi;
export const matchHashtag = /(^|[^\p{L}])#([\p{L}\p{N}]+)/giu;
export const matchLink = /https?:\/\/([a-zA-Z0-9\.\-]+\.[a-zA-Z]+)([\p{Letter}\p{Number}&\.-\/\?=#\-@%\+_,:]*)/gu;