mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-04-09 23:16:47 +02:00
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:
@@ -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
|
||||
20
README.md
20
README.md
@@ -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:
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user