Feature: Image Proxy (#122)

* feat: Implement image proxying functionality in KIND20Card and QuickViewKind20NoteCard components

* fix: Enhance image error handling in KIND20Card and QuickViewKind20NoteCard components

* feat: Add imgproxy configuration instructions to README for image proxying support

---------

Co-authored-by: highperfocused <highperfocused@pm.me>
This commit is contained in:
mroxso
2025-05-25 22:28:01 +02:00
committed by GitHub
parent 8b53f5da79
commit 8d7d028b88
5 changed files with 94 additions and 27 deletions

View File

@@ -2,4 +2,7 @@ NEXT_PUBLIC_SHOW_GEYSER_FUND=false
NEXT_PUBLIC_ENABLE_UMAMI=false
NEXT_PUBLIC_UMAMI_URL=https://your-umami-url.com
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-umami-website-id
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-umami-website-id
NEXT_PUBLIC_ENABLE_IMGPROXY=false
NEXT_PUBLIC_IMGPROXY_URL=https://your-imgproxy-url.com

View File

@@ -67,6 +67,26 @@ docker run -p 3000:3000 lumina
## ⚙️ Configuration
### Image Proxy (imgproxy)
LUMINA supports image proxying via imgproxy to optimize image loading, resize images on-the-fly, and enhance privacy. To enable:
1. Edit the `.env` file in the `lumina` directory:
```
NEXT_PUBLIC_ENABLE_IMGPROXY=true
NEXT_PUBLIC_IMGPROXY_URL=https://your-imgproxy-instance.com/
```
2. Make sure your imgproxy instance is properly configured and accessible.
3. Restart the application to apply changes.
The imgproxy feature:
- Resizes images to appropriate dimensions for better performance
- Falls back to direct image URLs if the proxy fails
- Provides faster loading times for large images
- Can be disabled by setting `NEXT_PUBLIC_ENABLE_IMGPROXY=false`
### Umami Analytics
Umami analytics is disabled by default. To enable:

View File

@@ -14,6 +14,7 @@ import ZapButton from "./ZapButton"
import Image from "next/image"
import CardOptionsDropdown from "./CardOptionsDropdown"
import { renderTextWithLinkedTags } from "@/utils/textUtils"
import { getProxiedImageUrl } from "@/utils/utils"
// Function to extract all images from a kind 20 event's imeta tags
const extractImagesFromEvent = (tags: string[][]): string[] => {
@@ -26,6 +27,8 @@ const extractImagesFromEvent = (tags: string[][]): string[] => {
.filter(Boolean) as string[]
}
const useImgProxy = process.env.NEXT_PUBLIC_ENABLE_IMGPROXY === "true"
interface KIND20CardProps {
pubkey: string
text: string
@@ -50,6 +53,7 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
})
const [currentImage, setCurrentImage] = useState(0);
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
const [imagesWithoutProxy, setImagesWithoutProxy] = useState<Record<string, boolean>>({});
const [api, setApi] = useState<any>(null);
// Extract all images from imeta tags
@@ -61,12 +65,21 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
// Filter out images with errors
const validImages = allImages.filter(img => !imageErrors[img]);
// Handle image error by marking that specific image as having an error
// Handle image error by first trying without proxy, then marking as error if that fails too
const handleImageError = (errorImage: string) => {
setImageErrors(prev => ({
...prev,
[errorImage]: true
}));
if (imagesWithoutProxy[errorImage]) {
// Already tried without proxy, mark as error
setImageErrors(prev => ({
...prev,
[errorImage]: true
}));
} else {
// Try without proxy
setImagesWithoutProxy(prev => ({
...prev,
[errorImage]: true
}));
}
}
// Update current image index when carousel slides
@@ -134,25 +147,29 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
setApi={setApi}
>
<CarouselContent>
{validImages.map((imageUrl, index) => (
<CarouselItem key={`${imageUrl}-${index}`}>
<div className="w-full flex justify-center">
<div className="relative w-full h-auto min-h-[300px] max-h-[80vh] flex justify-center">
<img
src={imageUrl}
alt={text}
className="rounded-lg w-full h-auto object-contain"
// onError={() => handleImageError(imageUrl)}
loading="lazy"
style={{
maxHeight: "80vh",
margin: "auto"
}}
/>
{validImages.map((imageUrl, index) => {
const shouldUseProxy = useImgProxy && !imagesWithoutProxy[imageUrl];
const image = shouldUseProxy ? getProxiedImageUrl(imageUrl, 1200, 0) : imageUrl;
return (
<CarouselItem key={`${imageUrl}-${index}`}>
<div className="w-full flex justify-center">
<div className="relative w-full h-auto min-h-[300px] max-h-[80vh] flex justify-center">
<img
src={image}
alt={text}
className="rounded-lg w-full h-auto object-contain"
onError={() => handleImageError(imageUrl)}
loading="lazy"
style={{
maxHeight: "80vh",
margin: "auto"
}}
/>
</div>
</div>
</div>
</CarouselItem>
))}
</CarouselItem>
);
})}
</CarouselContent>
{validImages.length > 1 && (
<>

View File

@@ -9,7 +9,7 @@ import {
} from "@/components/ui/card"
import Link from 'next/link';
import Image from 'next/image';
import { extractDimensions } from '@/utils/utils';
import { extractDimensions, getProxiedImageUrl } from '@/utils/utils';
interface QuickViewKind20NoteCardProps {
pubkey: string;
@@ -26,8 +26,14 @@ const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({ pubke
pubkey,
});
const [imageError, setImageError] = useState(false);
const [tryWithoutProxy, setTryWithoutProxy] = useState(false);
if (!image || !image.startsWith("http") || imageError) return null;
if (!image || !image.startsWith("http")) return null;
if (imageError && tryWithoutProxy) return null;
const useImgProxy = process.env.NEXT_PUBLIC_ENABLE_IMGPROXY === "true" && !tryWithoutProxy;
image = useImgProxy ? getProxiedImageUrl(image, 500, 0) : image;
text = text.replaceAll('\n', ' ');
const encodedNoteId = nip19.noteEncode(event.id)
@@ -44,7 +50,13 @@ const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({ pubke
alt={text}
className='w-full h-full rounded lg:rounded-lg object-cover'
loading="lazy"
// onError={() => setImageError(true)}
onError={() => {
if (tryWithoutProxy) {
setImageError(true);
} else {
setTryWithoutProxy(true);
}
}}
style={{ objectPosition: 'center' }}
/>
</div>

View File

@@ -54,4 +54,19 @@ export async function signEvent(loginType: string | null, event: NostrEvent): Pr
}
console.log(eventSigned);
return eventSigned;
}
// Create proxied image URL
export const getProxiedImageUrl = (url: string, width: number, height: number) => {
if (!url.startsWith("http")) return url;
try {
// Encode the URL to be used in the proxy
const encodedUrl = encodeURIComponent(url);
const imgproxyEnv = process.env.NEXT_PUBLIC_IMGPROXY_URL;
const imgproxyUrl = new URL(imgproxyEnv || "https://imgproxy.example.com");
return `${imgproxyUrl}_/resize:fit:${width}:${height}/plain/${encodedUrl}`;
} catch (error) {
console.error("Error creating proxied image URL:", error);
return url;
}
}