2023-10-01 23:03:52 +00:00
|
|
|
import styles from './text.module.css'
|
2023-10-03 00:07:05 +00:00
|
|
|
import { useState, useEffect, useMemo, useCallback } from 'react'
|
|
|
|
import { IMGPROXY_URL_REGEXP } from '../lib/url'
|
2023-10-01 23:03:52 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-10-03 00:07:05 +00:00
|
|
|
function ImageOriginal ({ src, topLevel, nofollow, tab, children, onClick, ...props }) {
|
2023-10-01 23:03:52 +00:00
|
|
|
const me = useMe()
|
2023-10-03 00:07:05 +00:00
|
|
|
const [showImage, setShowImage] = useState(false)
|
2023-10-01 23:03:52 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2023-10-03 18:05:04 +00:00
|
|
|
if (me?.imgproxyOnly && tab !== 'preview') return
|
2023-10-03 00:07:05 +00:00
|
|
|
// make sure it's not a false negative by trying to load URL as <img>
|
|
|
|
const img = new window.Image()
|
|
|
|
img.onload = () => setShowImage(true)
|
|
|
|
img.src = src
|
2023-10-01 23:03:52 +00:00
|
|
|
|
|
|
|
return () => {
|
2023-10-03 00:07:05 +00:00
|
|
|
img.onload = null
|
|
|
|
img.src = ''
|
2023-10-01 23:03:52 +00:00
|
|
|
}
|
2023-10-03 00:07:05 +00:00
|
|
|
}, [src, showImage])
|
2023-10-01 23:03:52 +00:00
|
|
|
|
2023-10-03 18:05:04 +00:00
|
|
|
if (showImage) {
|
2023-10-03 00:07:05 +00:00
|
|
|
return (
|
|
|
|
<img
|
|
|
|
className={topLevel ? styles.topLevel : undefined}
|
|
|
|
src={src}
|
|
|
|
onClick={() => onClick(src)}
|
|
|
|
onError={() => setShowImage(false)}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
} else {
|
2023-10-03 18:05:04 +00:00
|
|
|
// user is not okay with loading original url automatically or there was an error loading the image
|
|
|
|
|
|
|
|
// If element parsed by markdown is a raw URL, we use src as the text to not mislead users.
|
|
|
|
// This will not be the case if [text](url) format is used. Then we will show what was chosen as text.
|
|
|
|
const isRawURL = /^https?:\/\//.test(children?.[0])
|
2023-10-03 00:07:05 +00:00
|
|
|
return (
|
|
|
|
<a
|
|
|
|
target='_blank'
|
|
|
|
rel={`noreferrer ${nofollow ? 'nofollow' : ''} noopener`}
|
|
|
|
href={src}
|
2023-10-03 18:05:04 +00:00
|
|
|
>{isRawURL ? src : children}
|
2023-10-03 00:07:05 +00:00
|
|
|
</a>
|
|
|
|
)
|
|
|
|
}
|
2023-10-01 23:03:52 +00:00
|
|
|
}
|
|
|
|
|
2023-10-03 00:07:05 +00:00
|
|
|
function ImageProxy ({ src, srcSet: srcSetObj, onClick, topLevel, onError, ...props }) {
|
2023-10-01 23:03:52 +00:00
|
|
|
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])
|
2023-10-03 00:07:05 +00:00
|
|
|
const sizes = srcSet ? `${(topLevel ? 100 : 66)}vw` : undefined
|
2023-10-01 23:03:52 +00:00
|
|
|
|
|
|
|
// get source url in best resolution
|
|
|
|
const bestResSrc = useMemo(() => {
|
2023-10-03 00:07:05 +00:00
|
|
|
if (!srcSetObj) return src
|
2023-10-01 23:03:52 +00:00
|
|
|
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])
|
|
|
|
|
2023-10-03 00:07:05 +00:00
|
|
|
return (
|
|
|
|
<img
|
|
|
|
className={topLevel ? styles.topLevel : undefined}
|
|
|
|
// browsers that don't support srcSet and sizes will use src. use best resolution possible in that case
|
|
|
|
src={bestResSrc}
|
|
|
|
srcSet={srcSet}
|
|
|
|
sizes={sizes}
|
|
|
|
onClick={() => onClick(bestResSrc)}
|
|
|
|
onError={onError}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
2023-10-01 23:03:52 +00:00
|
|
|
|
2023-10-04 18:47:09 +00:00
|
|
|
export default function ZoomableImage ({ src, srcSet, ...props }) {
|
2023-10-03 00:07:05 +00:00
|
|
|
const showModal = useShowModal()
|
|
|
|
|
2023-10-03 18:05:04 +00:00
|
|
|
// if `srcSet` is falsy, it means the image was not processed by worker yet
|
|
|
|
const [imgproxy, setImgproxy] = useState(!!srcSet || IMGPROXY_URL_REGEXP.test(src))
|
2023-10-03 00:07:05 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
const handleClick = useCallback((src) => showModal(close => {
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={styles.fullScreenContainer}
|
|
|
|
onClick={close}
|
|
|
|
>
|
|
|
|
<img className={styles.fullScreen} src={src} />
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}, {
|
2023-10-01 23:03:52 +00:00
|
|
|
fullScreen: true,
|
|
|
|
overflow: (
|
|
|
|
<Dropdown.Item
|
2023-10-06 20:54:46 +00:00
|
|
|
href={originalUrl} target='_blank'
|
|
|
|
rel={`noreferrer ${props.nofollow ? 'nofollow' : ''} noopener`}
|
2023-10-01 23:03:52 +00:00
|
|
|
>
|
2023-10-03 00:07:05 +00:00
|
|
|
open original
|
2023-10-01 23:03:52 +00:00
|
|
|
</Dropdown.Item>)
|
2023-10-03 00:07:05 +00:00
|
|
|
}), [showModal, originalUrl, styles])
|
2023-10-01 23:03:52 +00:00
|
|
|
|
|
|
|
if (!src) return null
|
|
|
|
|
2023-10-03 00:07:05 +00:00
|
|
|
if (imgproxy) {
|
2023-10-01 23:03:52 +00:00
|
|
|
return (
|
2023-10-03 00:07:05 +00:00
|
|
|
<ImageProxy
|
|
|
|
src={src} srcSet={srcSet}
|
|
|
|
onClick={handleClick} onError={() => setImgproxy(false)} {...props}
|
2023-10-01 23:03:52 +00:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-10-03 00:07:05 +00:00
|
|
|
return <ImageOriginal src={originalUrl} onClick={handleClick} {...props} />
|
2023-10-01 23:03:52 +00:00
|
|
|
}
|