Configure imgproxy timeouts + add click to load on imgproxy errors (#404)

* Configure imgproxy timeouts

* Use click to load on imgproxy errors

* Add setting for click to load

---------

Co-authored-by: ekzyis <ek@stacker.news>
This commit is contained in:
ekzyis 2023-08-15 19:55:16 +02:00 committed by GitHub
parent 45bad1219e
commit 670c567bff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 63 additions and 11 deletions

View File

@ -56,6 +56,9 @@ IMGPROXY_SALT=
IMGPROXY_ENABLE_WEBP_DETECTION=1 IMGPROXY_ENABLE_WEBP_DETECTION=1
IMGPROXY_MAX_ANIMATION_FRAMES=100 IMGPROXY_MAX_ANIMATION_FRAMES=100
IMGPROXY_MAX_SRC_RESOLUTION=200 IMGPROXY_MAX_SRC_RESOLUTION=200
IMGPROXY_READ_TIMEOUT=10
IMGPROXY_WRITE_TIMEOUT=10
IMGPROXY_DOWNLOAD_TIMEOUT=9
# IMGPROXY_DEVELOPMENT_ERRORS_MODE=1 # IMGPROXY_DEVELOPMENT_ERRORS_MODE=1
# IMGPROXY_ENABLE_DEBUG_HEADERS=true # IMGPROXY_ENABLE_DEBUG_HEADERS=true

View File

@ -23,7 +23,7 @@ export default gql`
setSettings(tipDefault: Int!, turboTipping: Boolean!, fiatCurrency: String!, noteItemSats: Boolean!, setSettings(tipDefault: Int!, turboTipping: Boolean!, fiatCurrency: String!, noteItemSats: Boolean!,
noteEarning: Boolean!, noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!, noteEarning: Boolean!, noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
noteInvites: Boolean!, noteJobIndicator: Boolean!, noteCowboyHat: Boolean!, hideInvoiceDesc: Boolean!, noteInvites: Boolean!, noteJobIndicator: Boolean!, noteCowboyHat: Boolean!, hideInvoiceDesc: Boolean!,
hideFromTopUsers: Boolean!, hideCowboyHat: Boolean!, hideFromTopUsers: Boolean!, hideCowboyHat: Boolean!, clickToLoadImg: Boolean!,
wildWestMode: Boolean!, greeterMode: Boolean!, nostrPubkey: String, nostrRelays: [String!]): User wildWestMode: Boolean!, greeterMode: Boolean!, nostrPubkey: String, nostrRelays: [String!]): User
setPhoto(photoId: ID!): Int! setPhoto(photoId: ID!): Int!
upsertBio(bio: String!): User! upsertBio(bio: String!): User!
@ -80,6 +80,7 @@ export default gql`
hideInvoiceDesc: Boolean! hideInvoiceDesc: Boolean!
hideFromTopUsers: Boolean! hideFromTopUsers: Boolean!
hideCowboyHat: Boolean! hideCowboyHat: Boolean!
clickToLoadImg: Boolean!
wildWestMode: Boolean! wildWestMode: Boolean!
greeterMode: Boolean! greeterMode: Boolean!
lastCheckedJobs: String lastCheckedJobs: String

View File

@ -17,6 +17,7 @@ import copy from 'clipboard-copy'
import { IMGPROXY_URL_REGEXP, IMG_URL_REGEXP } from '../lib/url' import { IMGPROXY_URL_REGEXP, IMG_URL_REGEXP } from '../lib/url'
import { extractUrls } from '../lib/md' import { extractUrls } from '../lib/md'
import FileMissing from '../svgs/file-warning-line.svg' import FileMissing from '../svgs/file-warning-line.svg'
import { useMe } from './me'
function searchHighlighter () { function searchHighlighter () {
return (tree) => { return (tree) => {
@ -35,6 +36,15 @@ function searchHighlighter () {
} }
} }
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
}
function Heading ({ h, slugger, noFragments, topLevel, children, node, ...props }) { function Heading ({ h, slugger, noFragments, topLevel, children, node, ...props }) {
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [id] = useState(noFragments ? undefined : slugger.slug(toString(node).replace(/[^\w\-\s]+/gi, ''))) const [id] = useState(noFragments ? undefined : slugger.slug(toString(node).replace(/[^\w\-\s]+/gi, '')))
@ -183,8 +193,16 @@ export default memo(function Text ({ topLevel, noFragments, nofollow, onlyImgPro
) )
}) })
function ClickToLoad ({ children }) {
const [clicked, setClicked] = useState(false)
return clicked ? children : <div className='m-1 fst-italic pointer text-muted' onClick={() => setClicked(true)}>click to load image</div>
}
export function ZoomableImage ({ src, topLevel, ...props }) { export function ZoomableImage ({ src, topLevel, ...props }) {
const me = useMe()
const [err, setErr] = useState() const [err, setErr] = useState()
const [imgSrc, setImgSrc] = useState(src)
const [isImgProxy, setIsImgProxy] = useState(IMGPROXY_URL_REGEXP.test(src))
const defaultMediaStyle = { const defaultMediaStyle = {
maxHeight: topLevel ? '75vh' : '25vh', maxHeight: topLevel ? '75vh' : '25vh',
cursor: 'zoom-in' cursor: 'zoom-in'
@ -199,19 +217,31 @@ export function ZoomableImage ({ src, topLevel, ...props }) {
if (!src) return null if (!src) return null
if (err) { if (err) {
return ( if (!isImgProxy) {
<span className='d-flex align-items-baseline text-warning-emphasis fw-bold pb-1'> return (
<FileMissing width={18} height={18} className='fill-warning me-1 align-self-center' /> <span className='d-flex align-items-baseline text-warning-emphasis fw-bold pb-1'>
broken image <small className='ms-1'>stacker probably used an unreliable host</small> <FileMissing width={18} height={18} className='fill-warning me-1 align-self-center' />
</span> image error
) </span>
)
}
try {
const originalUrl = decodeOriginalUrl(src)
setImgSrc(originalUrl)
setErr(null)
} catch (err) {
console.error(err)
setErr(err)
}
// always set to false since imgproxy returned error
setIsImgProxy(false)
} }
return ( const img = (
<img <img
className={topLevel ? styles.topLevel : undefined} className={topLevel ? styles.topLevel : undefined}
style={mediaStyle} style={mediaStyle}
src={src} src={imgSrc}
onClick={() => { onClick={() => {
if (mediaStyle.cursor === 'zoom-in') { if (mediaStyle.cursor === 'zoom-in') {
setMediaStyle({ setMediaStyle({
@ -226,4 +256,11 @@ export function ZoomableImage ({ src, topLevel, ...props }) {
{...props} {...props}
/> />
) )
return (
(!me || !me.clickToLoadImg || isImgProxy)
? img
: <ClickToLoad>{img}</ClickToLoad>
)
} }

View File

@ -29,6 +29,7 @@ export const ME = gql`
hideInvoiceDesc hideInvoiceDesc
hideFromTopUsers hideFromTopUsers
hideCowboyHat hideCowboyHat
clickToLoadImg
wildWestMode wildWestMode
greeterMode greeterMode
lastCheckedJobs lastCheckedJobs
@ -51,6 +52,7 @@ export const SETTINGS_FIELDS = gql`
hideInvoiceDesc hideInvoiceDesc
hideFromTopUsers hideFromTopUsers
hideCowboyHat hideCowboyHat
clickToLoadImg
nostrPubkey nostrPubkey
nostrRelays nostrRelays
wildWestMode wildWestMode
@ -79,13 +81,13 @@ ${SETTINGS_FIELDS}
mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency: String!, $noteItemSats: Boolean!, mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency: String!, $noteItemSats: Boolean!,
$noteEarning: Boolean!, $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!, $noteEarning: Boolean!, $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!,
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $noteCowboyHat: Boolean!, $hideInvoiceDesc: Boolean!, $noteInvites: Boolean!, $noteJobIndicator: Boolean!, $noteCowboyHat: Boolean!, $hideInvoiceDesc: Boolean!,
$hideFromTopUsers: Boolean!, $hideCowboyHat: Boolean!, $hideFromTopUsers: Boolean!, $hideCowboyHat: Boolean!, $clickToLoadImg: Boolean!,
$wildWestMode: Boolean!, $greeterMode: Boolean!, $nostrPubkey: String, $nostrRelays: [String!]) { $wildWestMode: Boolean!, $greeterMode: Boolean!, $nostrPubkey: String, $nostrRelays: [String!]) {
setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency, setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency,
noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants, noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites, noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
noteJobIndicator: $noteJobIndicator, noteCowboyHat: $noteCowboyHat, hideInvoiceDesc: $hideInvoiceDesc, noteJobIndicator: $noteJobIndicator, noteCowboyHat: $noteCowboyHat, hideInvoiceDesc: $hideInvoiceDesc,
hideFromTopUsers: $hideFromTopUsers, hideCowboyHat: $hideCowboyHat, hideFromTopUsers: $hideFromTopUsers, hideCowboyHat: $hideCowboyHat, clickToLoadImg: $clickToLoadImg,
wildWestMode: $wildWestMode, greeterMode: $greeterMode, nostrPubkey: $nostrPubkey, nostrRelays: $nostrRelays) { wildWestMode: $wildWestMode, greeterMode: $greeterMode, nostrPubkey: $nostrPubkey, nostrRelays: $nostrRelays) {
...SettingsFields ...SettingsFields
} }

View File

@ -69,6 +69,7 @@ export default function Settings ({ ssrData }) {
hideInvoiceDesc: settings?.hideInvoiceDesc, hideInvoiceDesc: settings?.hideInvoiceDesc,
hideFromTopUsers: settings?.hideFromTopUsers, hideFromTopUsers: settings?.hideFromTopUsers,
hideCowboyHat: settings?.hideCowboyHat, hideCowboyHat: settings?.hideCowboyHat,
clickToLoadImg: settings?.clickToLoadImg,
wildWestMode: settings?.wildWestMode, wildWestMode: settings?.wildWestMode,
greeterMode: settings?.greeterMode, greeterMode: settings?.greeterMode,
nostrPubkey: settings?.nostrPubkey ? bech32encode(settings.nostrPubkey) : '', nostrPubkey: settings?.nostrPubkey ? bech32encode(settings.nostrPubkey) : '',
@ -215,6 +216,11 @@ export default function Settings ({ ssrData }) {
<Checkbox <Checkbox
label={<>hide my cowboy hat</>} label={<>hide my cowboy hat</>}
name='hideCowboyHat' name='hideCowboyHat'
groupClassName='mb-0'
/>
<Checkbox
label={<>click to load external images</>}
name='clickToLoadImg'
/> />
<div className='form-label'>content</div> <div className='form-label'>content</div>
<Checkbox <Checkbox

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "clickToLoadImg" BOOLEAN NOT NULL DEFAULT false;

View File

@ -49,6 +49,7 @@ model User {
fiatCurrency String @default("USD") fiatCurrency String @default("USD")
hideFromTopUsers Boolean @default(false) hideFromTopUsers Boolean @default(false)
turboTipping Boolean @default(false) turboTipping Boolean @default(false)
clickToLoadImg Boolean @default(false)
referrerId Int? referrerId Int?
nostrPubkey String? nostrPubkey String?
nostrAuthPubkey String? @unique(map: "users.nostrAuthPubkey_unique") nostrAuthPubkey String? @unique(map: "users.nostrAuthPubkey_unique")