diff --git a/components/block-height.js b/components/block-height.js index 1b347b2a..dae0d60d 100644 --- a/components/block-height.js +++ b/components/block-height.js @@ -20,7 +20,7 @@ export const BlockHeightProvider = ({ blockHeight, children }) => { }) const value = useMemo(() => ({ height: data?.blockHeight ?? blockHeight ?? 0 - }), [data, blockHeight]) + }), [data?.blockHeight, blockHeight]) return ( {children} diff --git a/components/chain-fee.js b/components/chain-fee.js index 10a358e8..9171292b 100644 --- a/components/chain-fee.js +++ b/components/chain-fee.js @@ -20,7 +20,7 @@ export const ChainFeeProvider = ({ chainFee, children }) => { }) const value = useMemo(() => ({ fee: Math.floor(data?.chainFee ?? chainFee ?? 0) - }), [data, chainFee]) + }), [data?.chainFee, chainFee]) return ( {children} diff --git a/components/image.js b/components/image.js index 6fec22d7..6cfa1f77 100644 --- a/components/image.js +++ b/components/image.js @@ -1,5 +1,5 @@ import styles from './text.module.css' -import { Fragment, useState, useEffect, useMemo, useCallback, forwardRef, useRef } from 'react' +import { Fragment, useState, useEffect, useMemo, useCallback, forwardRef, useRef, memo } from 'react' import { IMGPROXY_URL_REGEXP } from '../lib/url' import { useShowModal } from './modal' import { useMe } from './me' @@ -63,7 +63,7 @@ function ImageOriginal ({ src, topLevel, rel, tab, children, onClick, ...props } } } -function ImageProxy ({ src, srcSet: srcSetObj, onClick, topLevel, onError, ...props }) { +function ImageProxy ({ src, srcSet: { dimensions, ...srcSetObj } = {}, onClick, topLevel, onError, ...props }) { const srcSet = useMemo(() => { if (!srcSetObj) return undefined // srcSetObj shape: { [widthDescriptor]: , ... } @@ -82,19 +82,45 @@ function ImageProxy ({ src, srcSet: srcSetObj, onClick, topLevel, onError, ...pr }, { w: 0, url: undefined }).url }, [srcSetObj]) + const handleError = useCallback(onError, [onError]) + const handleClick = useCallback(() => onClick(bestResSrc), [onClick, bestResSrc]) + return ( - onClick(bestResSrc)} - onError={onError} + width={dimensions?.width} + height={dimensions?.height} + onClick={handleClick} + onError={handleError} /> ) } +const Image = memo(({ className, src, srcSet, sizes, width, height, bestResSrc, onClick, onError }) => { + const style = width && height + ? { '--height': `${height}px`, '--width': `${width}px`, '--aspect-ratio': `${width / height}` } + : undefined + + return ( + + ) +}) + export default function ZoomableImage ({ src, srcSet, ...props }) { const showModal = useShowModal() @@ -125,13 +151,15 @@ export default function ZoomableImage ({ src, srcSet, ...props }) { ) }), [showModal, originalUrl, styles]) + const handleError = useCallback(() => setImgproxy(false), [setImgproxy]) + if (!src) return null if (imgproxy) { return ( setImgproxy(false)} {...props} + onClick={handleClick} onError={handleError} {...props} /> ) } diff --git a/components/text.js b/components/text.js index 9f8583ad..5a3a1116 100644 --- a/components/text.js +++ b/components/text.js @@ -20,6 +20,7 @@ import { Button } from 'react-bootstrap' import { useRouter } from 'next/router' import Link from 'next/link' import { UNKNOWN_LINK_REL } from '../lib/constants' +import isEqual from 'lodash/isEqual' export function SearchText ({ text }) { return ( @@ -34,7 +35,7 @@ export function SearchText ({ text }) { } // this is one of the slowest components to render -export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, outlawed, ...outerProps }) { +export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, outlawed, topLevel, noFragments }) { const [overflowing, setOverflowing] = useState(false) const router = useRouter() const [show, setShow] = useState(false) @@ -79,7 +80,6 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o const Heading = useCallback(({ children, node, ...props }) => { const [copied, setCopied] = useState(false) - const { noFragments, topLevel } = outerProps const id = useMemo(() => noFragments ? undefined : slugger?.slug(toString(node).replace(/[^\w\-\s]+/gi, '')), [node, noFragments, slugger]) const h = useMemo(() => { @@ -114,7 +114,7 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o } ) - }, [outerProps, slugger.current]) + }, [topLevel, noFragments, slugger.current]) const Table = useCallback(({ node, ...props }) => @@ -144,8 +144,8 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o return url } const srcSet = imgproxyUrls?.[url] - return - }, [imgproxyUrls, outerProps, tab]) + return + }, [imgproxyUrls, topLevel, tab]) return (
@@ -208,7 +208,7 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o const youtube = href.match(/(https?:\/\/)?((www\.)?(youtube(-nocookie)?|youtube.googleapis)\.com.*(v\/|v=|vi=|vi\/|e\/|embed\/|user\/.*\/u\/\d+\/)|youtu\.be\/)(?[_0-9a-z-]+)((?:\?|&)(?:t|start)=(?\d+))?/i) if (youtube?.groups?.id) { return ( -
+
}
) -}) +}, isEqual) diff --git a/components/text.module.css b/components/text.module.css index 20ced3ad..9ffc0ea4 100644 --- a/components/text.module.css +++ b/components/text.module.css @@ -129,22 +129,26 @@ } .text img { + --height: 35vh; + --width: 100%; display: block; margin-top: .5rem; margin-bottom: .5rem; width: auto; - max-width: 100%; + max-width: min(var(--width), 100%); cursor: zoom-in; - max-height: 25vh; + height: auto; + max-height: min(var(--height), 25vh); object-fit: contain; object-position: left top; min-width: 50%; + aspect-ratio: var(--aspect-ratio); } .text img.topLevel { margin-top: .75rem; margin-bottom: .75rem; - max-height: 35vh; + max-height: min(var(--height), 35vh); } img.fullScreen { diff --git a/worker/imgproxy.js b/worker/imgproxy.js index 2d47d097..d72f20f7 100644 --- a/worker/imgproxy.js +++ b/worker/imgproxy.js @@ -1,6 +1,7 @@ import { createHmac } from 'node:crypto' import { extractUrls } from '../lib/md.js' import { isJob } from '../lib/item.js' +import path from 'node:path' const imgProxyEnabled = process.env.NODE_ENV === 'production' || (process.env.NEXT_PUBLIC_IMGPROXY_URL && process.env.IMGPROXY_SALT && process.env.IMGPROXY_KEY) @@ -100,21 +101,31 @@ export const createImgproxyUrls = async (id, text, { models, forceFetch }) => { console.log('[imgproxy] id:', id, '-- not image url:', url) continue } - imgproxyUrls[url] = {} + imgproxyUrls[url] = { + dimensions: await getDimensions(url) + } for (const res of resolutions) { const [w, h] = res.split('x') const processingOptions = `/rs:fit:${w}:${h}` - imgproxyUrls[url][`${w}w`] = createImgproxyUrl(url, processingOptions) + imgproxyUrls[url][`${w}w`] = createImgproxyUrl({ url, options: processingOptions }) } } return imgproxyUrls } -const createImgproxyUrl = (url, processingOptions) => { +const getDimensions = async (url) => { + const options = '/d:1' + const imgproxyUrl = createImgproxyUrl({ url, options, pathname: 'info' }) + const res = await fetch(imgproxyUrl) + const { width, height } = await res.json() + return { width, height } +} + +const createImgproxyUrl = ({ url, pathname = '', options }) => { const b64Url = Buffer.from(url, 'utf-8').toString('base64url') - const target = `${processingOptions}/${b64Url}` + const target = path.join(options, b64Url) const signature = sign(target) - return `${IMGPROXY_URL}${signature}${target}` + return new URL(path.join(pathname, signature, target), IMGPROXY_URL).toString() } async function fetchWithTimeout (resource, { timeout = 1000, ...options } = {}) {