222 lines
31 KiB
JavaScript
222 lines
31 KiB
JavaScript
|
import styles from './text.module.css'
|
||
|
import { useState, useRef, useEffect, useMemo, useCallback } from 'react'
|
||
|
import { extractUrls } from '../lib/md'
|
||
|
import { IMGPROXY_URL_REGEXP, IMG_URL_REGEXP } from '../lib/url'
|
||
|
import FileMissing from '../svgs/file-warning-line.svg'
|
||
|
import { useShowModal } from './modal'
|
||
|
import { useMe } from './me'
|
||
|
import { Dropdown } from 'react-bootstrap'
|
||
|
|
||
|
export function decodeOriginalUrl (imgproxyUrl) {
|
||
|
const parts = imgproxyUrl.split('/')
|
||
|
// base64url is not a known encoding in browsers
|
||
|
// so we need to replace the invalid chars
|
||
|
const b64Url = parts[parts.length - 1].replace(/-/g, '+').replace(/_/, '/')
|
||
|
const originalUrl = Buffer.from(b64Url, 'base64').toString('utf-8')
|
||
|
return originalUrl
|
||
|
}
|
||
|
|
||
|
export const IMG_CACHE_STATES = {
|
||
|
LOADING: 'IS_LOADING',
|
||
|
LOADED: 'IS_LOADED',
|
||
|
ERROR: 'IS_ERROR'
|
||
|
}
|
||
|
|
||
|
// this is the image at public/placeholder_click_to_load.png as a data URI so we don't have to rely on network to render it
|
||
|
const IMAGE_CLICK_TO_LOAD_DATA_URI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TxQ8qghYUEcxQnSyIijjWKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi6uKk6CIl/i8ptIj14Lgf7+497t4BQrXINKstAmi6bSZiUTGVXhU7XtGFAfRhBBMys4w5SYqj5fi6h4+vd2Ge1frcn6NHzVgM8InEEWaYNvEG8cymbXDeJw6yvKwSnxOPm3RB4keuKx6/cc65LPDMoJlMzBMHicVcEytNzPKmRjxNHFI1nfKFlMcq5y3OWrHM6vfkLwxk9JVlrtMcRgyLWIIEEQrKKKAIG2FadVIsJGg/2sI/5PolcinkKoCRYwElaJBdP/gf/O7Wyk5NekmBKND+4jgfo0DHLlCrOM73sePUTgD/M3ClN/ylKjD7SXqloYWOgN5t4OK6oSl7wOUOMPhkyKbsSn6aQjYLvJ/RN6WB/luge83rrb6P0wcgSV3Fb4CDQ2AsR9nrLd7d2dzbv2fq/f0A3Xly0Qz3JtMAAAAGYktHRABdAF0AWtYatQwAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnCRcSMjozlXYQAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAIABJREFUeNrt3XdAFHfiNvCHXVgWkBp6F7DSVLoiosGCUZPYsMQSRWJsp8Z45jSXaDR2DdGoMZr3zVmI0agpenZRwQZWBCUKCChYcEFpLmX39wdhZFxAzGGJPp+/YHfazs48822zozVs2BA1iOi1JOEuIGIAEBEDgIgYAETEACAiBgARMQCIiAFARAwAImIAEBEDgIgYAETEACAiBgARMQCIiAFARAwAImIAEBEDgIgYAETEACAiBgARMQCIiAFARAwAImIAEBEDgIgYAETEACAiBgARMQCIiAFARAwAImIAEBEDgIgYAETEACBiABARA4CIGABExAAgIgYAETEAiIgBQEQMACJiABARA4CIGABExAAgIgYAETEAiIgBQEQMACJiABARA4CIGABExAAgIgYAETEAiIgBQEQMACJiABARA4CIGABExAAgIgYAETEAiIgBQEQMACJiABARA4CIGABExAAgIgYAEQOAiBgARMQAICIGABExAIiIAUBEDAAiYgAQEQOAiBgARMQAICIGABExAIiIAUBEDAAiYgAQEQOAiBgArzhXVzfuBBJxcHBkADwPzs4uDZ7W19cPQUHtG3X9crkcrVq15hH/nL/Ll521tQ0cHZ0YAM+al5dXg6YbPHgohg0bgcTExEZdf0BAEKRSKc/e5/hd/h3o6urCza0ZA+BZcnNrDiMjo3qnMTY2wcyZnyI8vCdOnTqF8vKyRt0Gd3d3lJWVvTT7RE9PH/36DfiblgCcX5ljUy6Xw9bWlgHwLPn4+NR78nl7t8GcOXPRokVLlJSU4NdfdzT6NjRt6oLy8vKXYn/Y2dlj9uwv8PDhw7/dd9myZSsYGBi8MsemTKYLKyvrl2qbtF+9EkAzXLqUVOt7AwZEoEePcOjo6AAAEhJOo6ioqFHXb2FhCQsLiyeWAAwMDGBoaIRbt3KfYRj6YdSo0ZBIJNi3b+/f7rts27bdSxOkjVMFkMHc3JwlgGdFS0sLDg4OUCqVotebNGmC6dM/Qe/efYSTv6ysDL/8srPRtyEwMAgSiaTeAPDzC8CAAYOe6cnfq1cfjBs3HoaGhrhw4fxTVXNsbe0QHv7WSxHmT1uV8vX1g59fwF9ep1QqxbRp06Gt3fjXRplMBjMzMwZAQw0Z8h6mTp3W4Om9vdtAX19fo7hrZWUNLS0tVFZWCq+Vl5fD3d3jict0cHDEvHkL4OHh2aBtaNGiJQBohBAA6OjIEBX1IXr37oOYmI3PbL+NGTMWAwYMFMIuIyMDoaFd0KtXH0REDMb770di/PiJmDZtOiZOnIw2bdqK5s/JuQl9fX3MmTPvhXVnamlpwd7eHmVlT1cCSExMQGhoKCZNmgwjI+OnXu/bb/eFl5c3Ro0a02ifxcDAANra2pDJZNDT04O1tc1Lc45Jvb09P38Zr+QTJvwDnTqFwtraBoaGRrhw4fwT5+vePRwuLi44efIEbtzIFl7Pz1cgPv4YLl1KgomJKczNzSGXy9G2bVs0a9YMV65cRmlpqcbyvLy8MXnyVFhYWMDDwxNnz55BcXH9VYbBg4dCLpfj6NFY3L17R3jdw8MT06Z9DEtLKyxaNL/Rqx41Szrt2rWDlpZWjc/hhbZt28Hd3QPNmzdH06ZNIZfLceHCBWzevBE5OTdFy3FyaooTJ+Lh4+OHvn37wcTEBJcuJUGtVj/Hthw/BAd3RHp6Os6dO9Pg+ZycmuLo0Vi8+24/dOvWHWVl5UhPT2vQvHK5HB988CF0dXVhb2+P27fviI6jv/Y5fOHj44fk5Evw9fWDs3NTXL9+HVlZmSwB1JWWs2Z9Bn9/f+G1Ll3eRPfuPZ44r4uLKwDUejIDQFraNSxbthirV69CeXk5tLS04OTkDJVK88Du1KkzJk2aDENDQwCAsbExpkyZCrlcXuf6mzdvCWPjqqtOdSlES0sLw4aNxJQpH8HAoAm+/joaCoWi0febk1NTzJ49F82bN693uqysLPzww//H1Kn/wPbt2zSqBoGB7dG+fdW4iNWrV6KgoABhYV2xYMEitG3b7rkdB56eXn+WpBreeDlkyHuwtLREUVER1q5dA11dXQwfPgL/+tcs2Ng8ufV9wIAIoQdJIpFgyJChMDEx/csXsZEjR+Ptt9/BL7/sEKoAAGBvb88qQG2srKzx6aefo1mzR32l586dQ2FhIQYMiKj3ANTRkcHOzq7eAACAfv0GICrqA+jo6EClUuGnn7agoCBfNE3fvgMwcuT7whd2//59nD9/DjY2tpg8+aN60t5H+Pvhw4dwdXXD3Lnz0bVrV6jVaqxf/x0yMtKeyb7z8vJCSkoKTpw4jrQ08ToqKyuRmnoF0dFfYdasT3Dw4P5alxEU1B7Dh48QGgyLi4vx7bdroFQqYWVljUmTJmPcuInPpWXe1dVFaKtp2Mk/DF5eXkhIOAUAuHLlMnburDrxWrZshc8/n4O+fevuCjUxMUVwcEfRa8bGxvjggw+fetsdHZ0wd+58+Pn5YdWqb4SqZ3WVzNr65ekJeGmqAC1atMRHH02DhYWlqD4XHb0M8fFxsLd3QFhYGJKSkvDgwf1aD96AgKrGn71792hM4+DgiKlTpyEoKEho4ElMTMDWrVs06s/du3eHRFKVjSUlJVixIhq//fYrFAoFgoLaw8bGFufOndXYhnff7Yc33ngDAKBWqzF06DCYmZlBpVJhy5YYHDt29Jntvz/+SMW5c2eQmJgAuVwuGkBz+vQpLFu2BLm5OXXOHxTUHqNGRSIlJQWxsYeE1+/dy0NFRQU8PDwgkUhgb2+P4OCOKCkpRWbm9WfyWeRyOQYNGgKpVIqkpCSkpl554snfo0cP/P7777h27arw+tWrf8DBwRG2tnbQ0dFBy5Yt4ePjh+zsbCgU90TLGDlyFFxcNEcdWlpaorJS9cRtqPbWW70xZkwUTExM8P3363HlymXhveDgEFhbW6O8vByHDh1kCaDmwTd16jRRcevKlctYsSIaAPDgwX0sX74EW7duxYgR76NJkyYay3B3dxf+rq2e3qlTqOgLLi0txbp134lKENOnf4KOHR9dBcrKyrB+/TrhSzx6NBYzZ34CY2MT9OrVR6P12MHBQfi/a9du0NXVBQCcP38ee/fueW77s2ZjZ1UbSP4T9/+oUZHQ0dHBrl2/abz/3//uQkLC6RpXSxOMHh2J6dM/wRtvNH63lp9fgKi3pj5Dh1ad/AUF+dizZ7fG+6tXr0Jubm6Nq7MjZsz4F0aOHC20k9jY2MLX169G6InDoVev3k8ckmxkZIyPP/4nIiIGQU9PD/v378PJk8cfK6VWXXiqLxIMgD8TMzIyCnp6esJrmZmZWLp
|
||
|
|
||
|
const IMAGE_PROCESSING_DATA_URI = 'data:image/png;base64,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
|
||
|
|
||
|
export function useImgUrlCache (text, imgproxyUrls) {
|
||
|
const ref = useRef({})
|
||
|
const [imgUrlCache, setImgUrlCache] = useState({})
|
||
|
const me = useMe()
|
||
|
|
||
|
const updateCache = (url, state) => setImgUrlCache((prev) => ({ ...prev, [url]: state }))
|
||
|
|
||
|
useEffect(() => {
|
||
|
const urls = extractUrls(text)
|
||
|
|
||
|
urls.forEach((url) => {
|
||
|
if (IMG_URL_REGEXP.test(url) || !!imgproxyUrls?.[url]) {
|
||
|
// it's probably an image if the regexp matches or if we processed the URL as an image in the worker
|
||
|
updateCache(url, IMG_CACHE_STATES.LOADED)
|
||
|
} else {
|
||
|
// don't use image detection by trying to load as an image if user opted-out of loading external images automatically
|
||
|
if (me?.clickToLoadImg) return
|
||
|
// make sure it's not a false negative by trying to load URL as <img>
|
||
|
const img = new window.Image()
|
||
|
ref.current[url] = img
|
||
|
|
||
|
updateCache(url, IMG_CACHE_STATES.LOADING)
|
||
|
|
||
|
const callback = (state) => {
|
||
|
updateCache(url, state)
|
||
|
delete ref.current[url]
|
||
|
}
|
||
|
img.onload = () => callback(IMG_CACHE_STATES.LOADED)
|
||
|
img.onerror = () => callback(IMG_CACHE_STATES.ERROR)
|
||
|
img.src = url
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return () => {
|
||
|
Object.values(ref.current).forEach((img) => {
|
||
|
img.onload = null
|
||
|
img.onerror = null
|
||
|
img.src = ''
|
||
|
})
|
||
|
}
|
||
|
}, [text])
|
||
|
|
||
|
return imgUrlCache
|
||
|
}
|
||
|
|
||
|
export function ZoomableImage ({ src, topLevel, srcSet: srcSetObj, ...props }) {
|
||
|
const me = useMe()
|
||
|
const showModal = useShowModal()
|
||
|
const [originalUrlConsent, setOriginalUrlConsent] = useState(!me ? true : !me.clickToLoadImg)
|
||
|
// if there is no srcset obj, image is still processing (srcSetObj === undefined) or it wasn't detected as an image by the worker (srcSetObj === null).
|
||
|
// we handle both cases the same as imgproxy errors.
|
||
|
const [imgproxyErr, setImgproxyErr] = useState(!srcSetObj)
|
||
|
const [originalErr, setOriginalErr] = useState()
|
||
|
|
||
|
// backwards compatibility:
|
||
|
// src may already be imgproxy url since we used to replace image urls with imgproxy urls
|
||
|
const originalUrl = IMGPROXY_URL_REGEXP.test(src) ? decodeOriginalUrl(src) : src
|
||
|
|
||
|
// we will fallback to the original error if there was an error with our image proxy
|
||
|
const loadOriginalUrl = !!imgproxyErr
|
||
|
|
||
|
const srcSet = useMemo(() => {
|
||
|
if (!srcSetObj) return undefined
|
||
|
// srcSetObj shape: { [widthDescriptor]: <imgproxyUrl>, ... }
|
||
|
return Object.entries(srcSetObj).reduce((acc, [wDescriptor, url], i, arr) => {
|
||
|
return acc + `${url} ${wDescriptor}` + (i < arr.length - 1 ? ', ' : '')
|
||
|
}, '')
|
||
|
}, [srcSetObj])
|
||
|
const sizes = `${(topLevel ? 100 : 66)}vw`
|
||
|
|
||
|
// get source url in best resolution
|
||
|
const bestResSrc = useMemo(() => {
|
||
|
if (!srcSetObj) return undefined
|
||
|
return Object.entries(srcSetObj).reduce((acc, [wDescriptor, url]) => {
|
||
|
const w = Number(wDescriptor.replace(/w$/, ''))
|
||
|
return w > acc.w ? { w, url } : acc
|
||
|
}, { w: 0, url: undefined }).url
|
||
|
}, [srcSetObj])
|
||
|
|
||
|
const onError = useCallback((err) => {
|
||
|
if (!imgproxyErr) {
|
||
|
// first error is imgproxy error since that was loaded
|
||
|
console.error('imgproxy image error:', err)
|
||
|
setImgproxyErr(true)
|
||
|
} else {
|
||
|
// second error is error from original url
|
||
|
console.error('original image error:', err)
|
||
|
setOriginalErr(true)
|
||
|
}
|
||
|
}, [setImgproxyErr, setOriginalErr, imgproxyErr, originalUrl])
|
||
|
|
||
|
const handleClick = useCallback(() => showModal(close => (
|
||
|
<div
|
||
|
className='d-grid w-100 h-100' style={{ placeContent: 'center' }} onClick={close}
|
||
|
>
|
||
|
<img
|
||
|
style={{ cursor: 'zoom-out', maxWidth: '100%', maxHeight: '100%', minHeight: 0, minWidth: 0 }}
|
||
|
// also load original url in fullscreen if the original url was loaded
|
||
|
src={loadOriginalUrl ? originalUrl : bestResSrc}
|
||
|
onError={onError}
|
||
|
{...props}
|
||
|
/>
|
||
|
</div>
|
||
|
), {
|
||
|
fullScreen: true,
|
||
|
overflow: (
|
||
|
<Dropdown.Item
|
||
|
href={originalUrl} target='_blank' rel='noreferrer'
|
||
|
>
|
||
|
{loadOriginalUrl ? 'open in new tab' : 'open original'}
|
||
|
</Dropdown.Item>)
|
||
|
}), [showModal, loadOriginalUrl, originalUrl, bestResSrc, onError, props])
|
||
|
|
||
|
if (!src) return null
|
||
|
|
||
|
if ((srcSetObj === undefined) && originalUrlConsent && !originalErr) {
|
||
|
// image is still processing and user is okay with loading original url automatically
|
||
|
return (
|
||
|
<img
|
||
|
className={topLevel ? styles.topLevel : undefined}
|
||
|
style={{ cursor: 'zoom-in', maxHeight: topLevel ? '35vh' : '25vh' }}
|
||
|
src={originalUrl}
|
||
|
onClick={handleClick}
|
||
|
onError={() => setOriginalErr(true)}
|
||
|
{...props}
|
||
|
/>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if ((srcSetObj === undefined) && !originalUrlConsent && !originalErr) {
|
||
|
// image is still processing and user is not okay with loading original url automatically
|
||
|
const { host } = new URL(originalUrl)
|
||
|
return (
|
||
|
<div style={{ width: '256px' }}>
|
||
|
<img
|
||
|
className={topLevel ? styles.topLevel : undefined}
|
||
|
src={IMAGE_PROCESSING_DATA_URI} width='256px' height='256px'
|
||
|
style={{ cursor: 'pointer' }} onClick={() => setOriginalUrlConsent(true)}
|
||
|
/>
|
||
|
<div className='text-muted fst-italic text-center'>click to load original from</div>
|
||
|
<div className='text-muted fst-italic text-center'>{host}</div>
|
||
|
</div>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if (originalErr) {
|
||
|
// we already tried original URL: degrade <img> to <a> tag
|
||
|
return (
|
||
|
<>
|
||
|
<span className='d-flex align-items-baseline text-warning-emphasis fw-bold pb-1'>
|
||
|
<FileMissing width={18} height={18} className='fill-warning me-1 align-self-center' />
|
||
|
failed to load image
|
||
|
</span>
|
||
|
<a target='_blank' href={originalUrl} rel='noreferrer'>{originalUrl}</a>
|
||
|
</>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if (imgproxyErr && !originalUrlConsent) {
|
||
|
// respect privacy setting that external images should not be loaded automatically
|
||
|
const { host } = new URL(originalUrl)
|
||
|
return (
|
||
|
<div style={{ width: '256px' }}>
|
||
|
<div className='d-flex align-items-baseline text-warning-emphasis fw-bold pb-1 justify-content-center'>
|
||
|
<FileMissing width={18} height={18} className='fill-warning me-1 align-self-center' />
|
||
|
image proxy error
|
||
|
</div>
|
||
|
<img
|
||
|
className={topLevel ? styles.topLevel : undefined}
|
||
|
src={IMAGE_CLICK_TO_LOAD_DATA_URI} width='256px' height='256px'
|
||
|
style={{ cursor: 'pointer' }} onClick={() => setOriginalUrlConsent(true)}
|
||
|
/>
|
||
|
<div className='text-muted fst-italic text-center'>from {host}</div>
|
||
|
</div>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<img
|
||
|
className={topLevel ? styles.topLevel : undefined}
|
||
|
style={{ cursor: 'zoom-in', maxHeight: topLevel ? '35vh' : '25vh' }}
|
||
|
// browsers that don't support srcSet and sizes will use src. use best resolution possible in that case
|
||
|
src={loadOriginalUrl ? originalUrl : bestResSrc}
|
||
|
// we need to disable srcset and sizes to force browsers to use src
|
||
|
srcSet={loadOriginalUrl ? undefined : srcSet}
|
||
|
sizes={loadOriginalUrl ? undefined : sizes}
|
||
|
onClick={handleClick}
|
||
|
onError={onError}
|
||
|
{...props}
|
||
|
/>
|
||
|
)
|
||
|
}
|