Compare commits

...

3 Commits

Author SHA1 Message Date
k00b 1cc897a7a3 don't enforce min-width on videos 2024-09-04 11:00:54 -05:00
k00b 5a00f7b825 allow video in CSP 2024-09-04 09:58:05 -05:00
Keyan 07b98c3253
Optout of display of images and video (show them as links) (#1358)
* optout of display of images/video

* fix disableFreebies warning in settings

* preview trusted images

Co-authored-by: ekzyis <ek@stacker.news>

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-09-04 09:23:06 -05:00
8 changed files with 52 additions and 23 deletions

View File

@ -84,6 +84,7 @@ export default gql`
hideIsContributor: Boolean! hideIsContributor: Boolean!
hideWalletBalance: Boolean! hideWalletBalance: Boolean!
imgproxyOnly: Boolean! imgproxyOnly: Boolean!
showImagesAndVideos: Boolean!
nostrCrossposting: Boolean! nostrCrossposting: Boolean!
nostrPubkey: String nostrPubkey: String
nostrRelays: [String!] nostrRelays: [String!]
@ -155,6 +156,7 @@ export default gql`
hideIsContributor: Boolean! hideIsContributor: Boolean!
hideWalletBalance: Boolean! hideWalletBalance: Boolean!
imgproxyOnly: Boolean! imgproxyOnly: Boolean!
showImagesAndVideos: Boolean!
nostrCrossposting: Boolean! nostrCrossposting: Boolean!
nostrPubkey: String nostrPubkey: String
nostrRelays: [String!] nostrRelays: [String!]

View File

@ -19,13 +19,25 @@ export function decodeOriginalUrl (imgproxyUrl) {
return originalUrl return originalUrl
} }
function ImageOriginal ({ src, topLevel, rel, tab, children, onClick, ...props }) { function LinkRaw ({ href, children, src, rel, onClick, ...props }) {
const isRawURL = /^https?:\/\//.test(children?.[0])
return (
// eslint-disable-next-line
<a
target='_blank'
rel={rel ?? UNKNOWN_LINK_REL}
href={src}
>{isRawURL || !children ? src : children}
</a>
)
}
function ImageOriginal ({ src, topLevel, tab, onClick, ...props }) {
const me = useMe() const me = useMe()
const [showImage, setShowImage] = useState(false) const [showImage, setShowImage] = useState(false)
const [showVideo, setShowVideo] = useState(false) const [showVideo, setShowVideo] = useState(false)
useEffect(() => { useEffect(() => {
if (me?.privates?.imgproxyOnly && tab !== 'preview') return
// make sure it's not a false negative by trying to load URL as <img> // make sure it's not a false negative by trying to load URL as <img>
const img = new window.Image() const img = new window.Image()
img.onload = () => setShowImage(true) img.onload = () => setShowImage(true)
@ -42,7 +54,9 @@ function ImageOriginal ({ src, topLevel, rel, tab, children, onClick, ...props }
} }
}, [src, showImage]) }, [src, showImage])
if (showImage) { const showMedia = (tab === 'preview' || (me?.privates?.showImagesAndVideos !== false && !me?.privates?.imgproxyOnly))
if (showImage && showMedia) {
return ( return (
<img <img
className={topLevel ? styles.topLevel : undefined} className={topLevel ? styles.topLevel : undefined}
@ -51,23 +65,14 @@ function ImageOriginal ({ src, topLevel, rel, tab, children, onClick, ...props }
onError={() => setShowImage(false)} onError={() => setShowImage(false)}
/> />
) )
} else if (showVideo) { } else if (showVideo && showMedia) {
return <video src={src} controls /> return <video src={src} controls />
} else { } else {
// user is not okay with loading original url automatically or there was an error loading the image // 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. // 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. // 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]) return <LinkRaw src={src} {...props} />
return (
// eslint-disable-next-line
<a
target='_blank'
rel={rel ?? UNKNOWN_LINK_REL}
href={src}
>{isRawURL || !children ? src : children}
</a>
)
} }
} }
@ -138,6 +143,7 @@ const Image = memo(({ className, src, srcSet, sizes, width, height, onClick, onE
export default function ZoomableImage ({ src, srcSet, ...props }) { export default function ZoomableImage ({ src, srcSet, ...props }) {
const showModal = useShowModal() const showModal = useShowModal()
const me = useMe()
// if `srcSet` is falsy, it means the image was not processed by worker yet // if `srcSet` is falsy, it means the image was not processed by worker yet
const [trustedDomain, setTrustedDomain] = useState(!!srcSet || IMGPROXY_URL_REGEXP.test(src) || MEDIA_DOMAIN_REGEXP.test(src)) const [trustedDomain, setTrustedDomain] = useState(!!srcSet || IMGPROXY_URL_REGEXP.test(src) || MEDIA_DOMAIN_REGEXP.test(src))
@ -171,12 +177,16 @@ export default function ZoomableImage ({ src, srcSet, ...props }) {
if (!src) return null if (!src) return null
if (trustedDomain) { if (trustedDomain) {
if (props.tab === 'preview' || !me || me.privates.showImagesAndVideos) {
return ( return (
<TrustedImage <TrustedImage
src={src} srcSet={srcSet} src={src} srcSet={srcSet}
onClick={handleClick} onError={handleError} {...props} onClick={handleClick} onError={handleError} {...props}
/> />
) )
} else {
return <LinkRaw src={src} onClick={handleClick} {...props} />
}
} }
return <ImageOriginal src={originalUrl} onClick={handleClick} {...props} /> return <ImageOriginal src={originalUrl} onClick={handleClick} {...props} />

View File

@ -135,13 +135,13 @@
max-width: 100%; max-width: 100%;
height: auto; height: auto;
max-height: 25vh; max-height: 25vh;
min-width: 50%;
aspect-ratio: var(--aspect-ratio); aspect-ratio: var(--aspect-ratio);
} }
.text img { .text img {
cursor: zoom-in; cursor: zoom-in;
object-fit: contain; object-fit: contain;
min-width: 50%;
object-position: left top; object-position: left top;
} }

View File

@ -26,6 +26,7 @@ export const ME = gql`
hideWalletBalance hideWalletBalance
hideWelcomeBanner hideWelcomeBanner
imgproxyOnly imgproxyOnly
showImagesAndVideos
lastCheckedJobs lastCheckedJobs
nostrCrossposting nostrCrossposting
noteAllDescendants noteAllDescendants
@ -98,6 +99,7 @@ export const SETTINGS_FIELDS = gql`
hideTwitter hideTwitter
hideIsContributor hideIsContributor
imgproxyOnly imgproxyOnly
showImagesAndVideos
hideWalletBalance hideWalletBalance
diagnostics diagnostics
noReferralLinks noReferralLinks

View File

@ -87,7 +87,7 @@ export function middleware (request) {
"font-src 'self' a.stacker.news", "font-src 'self' a.stacker.news",
// we want to load images from everywhere but we can limit to HTTPS at least // we want to load images from everywhere but we can limit to HTTPS at least
"img-src 'self' a.stacker.news m.stacker.news https: data: blob:" + devSrc, "img-src 'self' a.stacker.news m.stacker.news https: data: blob:" + devSrc,
"media-src 'self' a.stacker.news m.stacker.news" + devSrc, "media-src 'self' a.stacker.news m.stacker.news https:" + devSrc,
// Using nonces and strict-dynamic deploys a strict CSP. // Using nonces and strict-dynamic deploys a strict CSP.
// see https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html#strict-policy. // see https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html#strict-policy.
// Old browsers will ignore nonce and strict-dynamic and fallback to host-based matching and unsafe-inline // Old browsers will ignore nonce and strict-dynamic and fallback to host-based matching and unsafe-inline

View File

@ -116,7 +116,7 @@ export default function Settings ({ ssrData }) {
tipRandomMin: settings?.tipRandomMin || 1, tipRandomMin: settings?.tipRandomMin || 1,
tipRandomMax: settings?.tipRandomMax || 10, tipRandomMax: settings?.tipRandomMax || 10,
turboTipping: settings?.turboTipping, turboTipping: settings?.turboTipping,
disableFreebies: settings?.disableFreebies, disableFreebies: settings?.disableFreebies || undefined,
zapUndos: settings?.zapUndos || (settings?.tipDefault ? 100 * settings.tipDefault : 2100), zapUndos: settings?.zapUndos || (settings?.tipDefault ? 100 * settings.tipDefault : 2100),
zapUndosEnabled: settings?.zapUndos !== null, zapUndosEnabled: settings?.zapUndos !== null,
fiatCurrency: settings?.fiatCurrency || 'USD', fiatCurrency: settings?.fiatCurrency || 'USD',
@ -140,6 +140,7 @@ export default function Settings ({ ssrData }) {
hideNostr: settings?.hideNostr, hideNostr: settings?.hideNostr,
hideTwitter: settings?.hideTwitter, hideTwitter: settings?.hideTwitter,
imgproxyOnly: settings?.imgproxyOnly, imgproxyOnly: settings?.imgproxyOnly,
showImagesAndVideos: settings?.showImagesAndVideos,
wildWestMode: settings?.wildWestMode, wildWestMode: settings?.wildWestMode,
satsFilter: settings?.satsFilter, satsFilter: settings?.satsFilter,
nsfwMode: settings?.nsfwMode, nsfwMode: settings?.nsfwMode,
@ -508,6 +509,17 @@ export default function Settings ({ ssrData }) {
required required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/> />
<Checkbox
label={
<div className='d-flex align-items-center'>show images and video
<Info>
<p>disable to show images and videos as links instead of embedding them</p>
</Info>
</div>
}
name='showImagesAndVideos'
groupClassName='mb-0'
/>
<Checkbox <Checkbox
label={ label={
<div className='d-flex align-items-center'>wild west mode <div className='d-flex align-items-center'>wild west mode

View File

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

View File

@ -63,6 +63,7 @@ model User {
turboTipping Boolean @default(false) turboTipping Boolean @default(false)
zapUndos Int? zapUndos Int?
imgproxyOnly Boolean @default(false) imgproxyOnly Boolean @default(false)
showImagesAndVideos Boolean @default(true)
hideWalletBalance Boolean @default(false) hideWalletBalance Boolean @default(false)
disableFreebies Boolean? disableFreebies Boolean?
referrerId Int? referrerId Int?