From 01d4bb7b484ad0a3c3353d688aee13fd0d0d628c Mon Sep 17 00:00:00 2001 From: "xan.lol" Date: Tue, 2 Jun 2026 20:43:04 -0700 Subject: [PATCH] feat: set image formats (& readme edits) partially from @aylac.top https://github.com/ayla6/deer-social-test/ ty --- README.md | 19 +- src/components/Lightbox/Lightbox.web.tsx | 7 +- src/components/MediaPreview.tsx | 6 +- .../Embed/ExternalEmbed/ExternalPlayer.tsx | 6 +- .../Post/Embed/ExternalEmbed/index.tsx | 6 +- src/components/Post/Embed/ImageEmbed.tsx | 34 +-- src/components/images/AutoSizedImage.tsx | 6 +- src/components/images/ImageLayoutGridItem.tsx | 6 +- src/lib/constants.ts | 6 +- src/lib/media/embed-image-formats.ts | 39 ++++ src/lib/media/image-formats.ts | 18 ++ src/lib/media/manip.ts | 27 ++- src/lib/media/manip.web.ts | 21 +- src/lib/media/save-image.ios.ts | 9 +- src/lib/media/save-image.ts | 9 +- src/lib/media/util.ts | 11 + src/screens/Profile/Header/Shell.tsx | 21 +- .../RunesSettings/DisplaySettings.tsx | 142 ++++++++++--- src/state/persisted/schema.ts | 21 +- src/state/preferences/download-format.tsx | 54 +++++ src/state/preferences/fullsize-format.tsx | 54 +++++ src/state/preferences/high-quality-images.tsx | 81 ------- src/state/preferences/image-cdn-host.tsx | 21 +- src/state/preferences/index.tsx | 197 +++++++++--------- src/state/preferences/load-small-pngs.tsx | 52 +++++ src/state/preferences/settings-sync.tsx | 5 +- src/state/preferences/thumbnail-format.tsx | 54 +++++ src/view/com/util/UserAvatar.tsx | 12 +- src/view/com/util/UserBanner.tsx | 13 +- 29 files changed, 665 insertions(+), 292 deletions(-) create mode 100644 src/lib/media/embed-image-formats.ts create mode 100644 src/lib/media/image-formats.ts create mode 100644 src/state/preferences/download-format.tsx create mode 100644 src/state/preferences/fullsize-format.tsx delete mode 100644 src/state/preferences/high-quality-images.tsx create mode 100644 src/state/preferences/load-small-pngs.tsx create mode 100644 src/state/preferences/thumbnail-format.tsx diff --git a/README.md b/README.md index bac7944df..4df7ce340 100644 --- a/README.md +++ b/README.md @@ -22,17 +22,18 @@ alt="Get it on Obtainium" height="54" /> --> - You can change "post" to any phrase you want in the UI - Choose between sharing witchsky.app or bsky.app links - Embed player works with [stream.place](https://stream.place/) links! -- Open posts in PDSls and original fedi-bridged pages +- Open posts in PDSls and original bridged fedi instances - You can delete and redraft (basically edit!) posts - Better defaults (alt text required 😉 autoplay off 🫨) - Read your chat threads with deleted/deactivated accounts -- More unique repost icons +- Unique repost icons in notifications +- Repost carousels in following feed - Can download videos - Stay on a page when switching accounts - 'Mutuals' in place of 'Following' when relevant - Pet label (option to toggle, & visibility for everyone else) - Account AI preferences screen -- No unread notifications badge limit (30+) +- No unread notifications badge limit (✗30+) - No age assurance or blocks based on location - No push notifications (hopefully will be added later) - Kept as up-to-date as possible (sporadically unstable as a result) @@ -88,10 +89,15 @@ These are all available as options in the Runes sub-page of the app's settings. - Account switcher - Use compact account switcher - Automatically use compact account switcher with more than 6 accounts - - Combine reposts into a horizontal carousel - - Display images in higher quality + - Anything else + - Compact posts - Show client used to post - Change custom post phrase + - Images + - Thumbnail format + - Full-size format + - Download format + - Load especially small images as PNGs - Infrastructure - Post translation provider - Google Translate @@ -120,13 +126,12 @@ These are all available as options in the Runes sub-page of the app's settings. ### TODO: Xan - [ ] Setup App Linking for Android (.well-known w/ app package fingerprint) -- [ ] Make fallback email addresses use witchsky.social in Legacy sign-in automatic PDS detection - [ ] Change followed accounts [on onboarding](https://github.com/blacksky-algorithms/blacksky.community/commit/e36ee43efb4999f070860d7f70122e45b28c1e2b) - [ ] Visual replies indicator like the [Firmament userstyle](https://witchsky.app/profile/did:plc:jwhxcrf5uvl3vyw7nurecgt5/post/3m4rr3vzmak2a) - [ ] Permit editing Menu options in Runes > Menus (remember [Outlinks page](https://witchsky.app/profile/did:plc:q7suwaz53ztc4mbiqyygbn43/post/3m5zjhhshic2g)?) - [ ] ShareMenuItems.tsx, ShareMenuItems.web.tsx - [ ] Add Open bridged OG fedi account page option to ProfileMenu.tsx -- [ ] Witchsky PDS and .social site (list good songs containing 'bitch' in their titles for related site) +- [ ] Witchsky PDS and .social site (list good songs containing 'bitch' in their titles on related site) ### Upcoming or wishful features diff --git a/src/components/Lightbox/Lightbox.web.tsx b/src/components/Lightbox/Lightbox.web.tsx index 5b6762342..fa9cd05cf 100644 --- a/src/components/Lightbox/Lightbox.web.tsx +++ b/src/components/Lightbox/Lightbox.web.tsx @@ -7,6 +7,7 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar' import {saveImageToMediaLibrary} from '#/lib/media/manip' import {useA11y} from '#/state/a11y' +import {useDownloadFormat} from '#/state/preferences/download-format' import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' import { atoms as a, @@ -107,6 +108,7 @@ function LightboxGallery({ const {gtPhone} = useBreakpoints() const enableSquareButtons = useEnableSquareButtons() + const downloadFormat = useDownloadFormat() const canGoLeft = index >= 1 const canGoRight = index < imgs.length - 1 @@ -309,7 +311,10 @@ function LightboxGallery({ { - saveImageToMediaLibrary({uri: img.uri}).then( + saveImageToMediaLibrary({ + uri: img.uri, + format: downloadFormat ?? 'jpeg', + }).then( () => { Toast.show(l`Image saved`) }, diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx index b61f2842b..a71419109 100644 --- a/src/components/MediaPreview.tsx +++ b/src/components/MediaPreview.tsx @@ -4,11 +4,11 @@ import {type AppBskyFeedDefs} from '@atproto/api' import {Trans} from '@lingui/react/macro' import {isGifEmbed} from '#/lib/strings/embed-player' -import {useHighQualityImages} from '#/state/preferences/high-quality-images' import { applyImageTransforms, useImageCdnHost, } from '#/state/preferences/image-cdn-host' +import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' import {atoms as a, useTheme} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '#/components/Typography' @@ -101,13 +101,13 @@ export function ImageItem({ children?: React.ReactNode }) { const t = useTheme() - const highQualityImages = useHighQualityImages() + const thumbnailFormat = useThumbnailFormat() const imageCdnHost = useImageCdnHost() const transformedThumbnail = thumbnail ? applyImageTransforms(thumbnail, { imageCdnHost, - highQualityImages, + format: thumbnailFormat, }) : undefined diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx index 4231319f3..2597ee61f 100644 --- a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx +++ b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx @@ -26,11 +26,11 @@ import { getPlayerAspect, } from '#/lib/strings/embed-player' import {useExternalEmbedsPrefs} from '#/state/preferences' -import {useHighQualityImages} from '#/state/preferences/high-quality-images' import { applyImageTransforms, useImageCdnHost, } from '#/state/preferences/image-cdn-host' +import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' import {EventStopper} from '#/view/com/util/EventStopper' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' @@ -136,7 +136,7 @@ export function ExternalPlayer({ const windowDims = useWindowDimensions() const externalEmbedsPrefs = useExternalEmbedsPrefs() const consentDialogControl = useDialogControl() - const highQualityImages = useHighQualityImages() + const thumbnailFormat = useThumbnailFormat() const imageCdnHost = useImageCdnHost() const [isPlayerActive, setIsPlayerActive] = useState(false) @@ -237,7 +237,7 @@ export function ExternalPlayer({ source={{ uri: applyImageTransforms(link.thumb, { imageCdnHost, - highQualityImages, + format: thumbnailFormat, }), }} accessibilityIgnoresInvertColors diff --git a/src/components/Post/Embed/ExternalEmbed/index.tsx b/src/components/Post/Embed/ExternalEmbed/index.tsx index cabc84847..cc135e94c 100644 --- a/src/components/Post/Embed/ExternalEmbed/index.tsx +++ b/src/components/Post/Embed/ExternalEmbed/index.tsx @@ -14,11 +14,11 @@ import { } from '#/lib/strings/embed-player' import {toNiceDomain} from '#/lib/strings/url-helpers' import {useExternalEmbedsPrefs} from '#/state/preferences' -import {useHighQualityImages} from '#/state/preferences/high-quality-images' import { applyImageTransforms, useImageCdnHost, } from '#/state/preferences/image-cdn-host' +import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' import {atoms as a, useTheme} from '#/alf' import {Divider} from '#/components/Divider' import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' @@ -44,13 +44,13 @@ export const ExternalEmbed = ({ const t = useTheme() const playHaptic = useHaptics() const externalEmbedPrefs = useExternalEmbedsPrefs() - const highQualityImages = useHighQualityImages() + const thumbnailFormat = useThumbnailFormat() const imageCdnHost = useImageCdnHost() const niceUrl = toNiceDomain(link.uri) const imageUri = link.thumb ? applyImageTransforms(link.thumb, { imageCdnHost, - highQualityImages, + format: thumbnailFormat, }) : undefined const embedPlayerParams = useMemo(() => { diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index 57489f3b0..b3af1abf0 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -2,11 +2,14 @@ import {InteractionManager, View} from 'react-native' import {type AnimatedRef} from 'react-native-reanimated' import {Image} from 'expo-image' -import {useHighQualityImages} from '#/state/preferences/high-quality-images' +import {resolveEmbedImageUris} from '#/lib/media/embed-image-formats' +import {useFullsizeFormat} from '#/state/preferences/fullsize-format' import { applyImageTransforms, useImageCdnHost, } from '#/state/preferences/image-cdn-host' +import {useLoadAsPngs} from '#/state/preferences/load-small-pngs' +import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' import {atoms as a, tokens} from '#/alf' import {AutoSizedImage} from '#/components/images/AutoSizedImage' import {Gallery} from '#/components/images/Gallery' @@ -26,24 +29,27 @@ export function ImageEmbed({ }) { const ax = useAnalytics() const {openLightbox} = useLightboxControls() - const highQualityImages = useHighQualityImages() + const fullsizeFormat = useFullsizeFormat() + const thumbnailFormat = useThumbnailFormat() + const loadAsPngs = useLoadAsPngs() const imageCdnHost = useImageCdnHost() const {images} = embed.view const galleryEnabled = ax.features.enabled(ax.features.PostGalleryEmbedEnable) if (images.length > 0) { - const items = images.map(img => ({ - uri: applyImageTransforms(img.fullsize, { - imageCdnHost, - highQualityImages, - }), - thumbUri: applyImageTransforms(img.thumb, { - imageCdnHost, - highQualityImages, - }), - alt: img.alt, - dimensions: img.aspectRatio ?? null, - })) + const items = images.map(img => { + const {fullsize, thumb} = resolveEmbedImageUris(img, { + thumbnailFormat: thumbnailFormat ?? 'webp', + fullsizeFormat: fullsizeFormat ?? 'webp', + loadAsPngs: loadAsPngs ?? true, + }) + return { + uri: applyImageTransforms(fullsize, {imageCdnHost}), + thumbUri: applyImageTransforms(thumb, {imageCdnHost}), + alt: img.alt, + dimensions: img.aspectRatio ?? null, + } + }) const onPress = ( index: number, refs: AnimatedRef[], diff --git a/src/components/images/AutoSizedImage.tsx b/src/components/images/AutoSizedImage.tsx index eefb5a368..04d3ad03a 100644 --- a/src/components/images/AutoSizedImage.tsx +++ b/src/components/images/AutoSizedImage.tsx @@ -12,12 +12,12 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {type Dimensions} from '#/lib/media/types' -import {useHighQualityImages} from '#/state/preferences/high-quality-images' import { applyImageTransforms, useImageCdnHost, } from '#/state/preferences/image-cdn-host' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' +import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' import {atoms as a, useTheme, web} from '#/alf' import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' import {MediaInsetBorder} from '#/components/MediaInsetBorder' @@ -90,7 +90,7 @@ export function AutoSizedImage({ const largeAlt = useLargeAltBadgeEnabled() const containerRef = useAnimatedRef() const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) - const highQualityImages = useHighQualityImages() + const thumbnailFormat = useThumbnailFormat() const imageCdnHost = useImageCdnHost() let aspectRatio: number | undefined @@ -124,7 +124,7 @@ export function AutoSizedImage({ style={[a.w_full, a.h_full]} source={applyImageTransforms(image.thumb, { imageCdnHost, - highQualityImages, + format: thumbnailFormat, })} accessible={true} // Must set for `accessibilityLabel` to work accessibilityIgnoresInvertColors diff --git a/src/components/images/ImageLayoutGridItem.tsx b/src/components/images/ImageLayoutGridItem.tsx index 8bf6c7e1d..8b053bfbc 100644 --- a/src/components/images/ImageLayoutGridItem.tsx +++ b/src/components/images/ImageLayoutGridItem.tsx @@ -8,12 +8,12 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {type Dimensions} from '#/lib/media/types' -import {useHighQualityImages} from '#/state/preferences/high-quality-images' import { applyImageTransforms, useImageCdnHost, } from '#/state/preferences/image-cdn-host' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' +import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' import {atoms as a, useTheme} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {PostEmbedViewContext} from '#/components/Post/Embed/types' @@ -53,7 +53,7 @@ export function GalleryItem({ const t = useTheme() const {_} = useLingui() const largeAltBadge = useLargeAltBadgeEnabled() - const highQualityImages = useHighQualityImages() + const thumbnailFormat = useThumbnailFormat() const imageCdnHost = useImageCdnHost() const image = images[index] const hasAlt = !!image.alt @@ -86,7 +86,7 @@ export function GalleryItem({ source={{ uri: applyImageTransforms(image.thumb, { imageCdnHost, - highQualityImages, + format: thumbnailFormat, }), }} style={[a.flex_1]} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 9d0fbc3ef..eb4621533 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -7,7 +7,8 @@ import { CHAT_PROXY_DID, ENV_APPVIEW_DID_PROXY, ENV_PUBLIC_BSKY_SERVICE, - IS_DEV} from '#/env' + IS_DEV, +} from '#/env' export const LOCAL_DEV_SERVICE = Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' export const STAGING_SERVICE = 'https://staging.bsky.dev' @@ -255,3 +256,6 @@ export const webLinks = { } export const DOH_ENDPOINT = 'https://cloudflare-dns.com/dns-query' + +export const PNG_IMG_MAX_BYTE = 100_000 +export const PNG_IMG_MAX_SIZE = 300 diff --git a/src/lib/media/embed-image-formats.ts b/src/lib/media/embed-image-formats.ts new file mode 100644 index 000000000..1507e0704 --- /dev/null +++ b/src/lib/media/embed-image-formats.ts @@ -0,0 +1,39 @@ +import {PNG_IMG_MAX_SIZE} from '#/lib/constants' +import {modifyImageFormat} from '#/lib/media/util' + +export function resolveEmbedImageUris( + img: { + thumb: string + fullsize: string + aspectRatio?: {width: number; height: number} | null + }, + { + thumbnailFormat, + fullsizeFormat, + loadAsPngs, + }: { + thumbnailFormat: string + fullsizeFormat: string + loadAsPngs: boolean + }, +) { + const lowRes = + img.aspectRatio && + img.aspectRatio.width <= 1000 && + img.aspectRatio.height <= 1000 + + const pngSized = + loadAsPngs && + img.aspectRatio && + img.aspectRatio.width <= PNG_IMG_MAX_SIZE && + img.aspectRatio.height <= PNG_IMG_MAX_SIZE + + const resolvedFullsizeFormat = pngSized ? 'png' : fullsizeFormat + const fullsize = modifyImageFormat(img.fullsize, resolvedFullsizeFormat) + const thumb = + pngSized && lowRes + ? fullsize + : modifyImageFormat(img.thumb, thumbnailFormat) + + return {fullsize, thumb} +} diff --git a/src/lib/media/image-formats.ts b/src/lib/media/image-formats.ts new file mode 100644 index 000000000..366cc0e66 --- /dev/null +++ b/src/lib/media/image-formats.ts @@ -0,0 +1,18 @@ +import {IS_NATIVE} from '#/env' + +export const IMAGE_FORMATS = [ + {label: 'AVIF', value: 'avif'}, + {label: 'WebP', value: 'webp'}, + {label: 'JPEG', value: 'jpeg'}, + {label: 'PNG', value: 'png'}, + {label: 'GIF', value: 'gif'}, + {label: 'BMP', value: 'bmp'}, + {label: 'ICO', value: 'ico'}, + {label: 'JPEG XL', value: 'jxl'}, + ...(IS_NATIVE ? [{label: 'HEIC', value: 'heic'}] : []), +] + +export function formatToFileExt(format: string) { + if (format === 'jpeg') return 'jpg' + return format +} diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index 5037cfc7d..1c219154b 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -17,11 +17,12 @@ import * as MediaLibrary from 'expo-media-library' import * as Sharing from 'expo-sharing' import {POST_IMG_MAX} from '#/lib/constants' +import {formatToFileExt} from '#/lib/media/image-formats' import {logger} from '#/logger' import {IS_ANDROID, IS_IOS} from '#/env' import {type PickerImage} from './picker.shared' import {type Dimensions} from './types' -import {convertCdnPreset} from './util' +import {getDownloadImageUri} from './util' import {mimeToExt} from './video/util' export async function compressIfNeeded( @@ -103,19 +104,29 @@ export async function shareImageModal({uri}: {uri: string}) { const ALBUM_NAME = 'Bluesky' /** - * Saves an image to the user's device. Uses the CDN's `download` preset - * which uses the JPEG version with the Content-Disposition header set to - * `attachment; filename=`. On native this saves to the media library; - * on web it triggers a browser download. + * Saves an image to the user's device. Uses the CDN's `download` preset with the + * chosen format suffix. On native this saves to the media library; on web it + * triggers a browser download. */ -export async function saveImageToMediaLibrary({uri}: {uri: string}) { - const downloadUri = convertCdnPreset(uri, 'download') +export async function saveImageToMediaLibrary({ + uri, + format = 'jpeg', +}: { + uri: string + format?: string +}) { + const downloadUri = getDownloadImageUri(uri, format) const downloadedPath = await downloadImage( downloadUri, String(uuid.v4()), 20e3, ) - const imagePath = await moveToPermanentPath(downloadedPath, '.jpg') + const dotIndex = downloadedPath.lastIndexOf('.') + const ext = + dotIndex >= 0 + ? downloadedPath.slice(dotIndex) + : `.${formatToFileExt(format)}` + const imagePath = await moveToPermanentPath(downloadedPath, ext) // save try { diff --git a/src/lib/media/manip.web.ts b/src/lib/media/manip.web.ts index fbd4d9551..cee8531a7 100644 --- a/src/lib/media/manip.web.ts +++ b/src/lib/media/manip.web.ts @@ -1,7 +1,8 @@ import {POST_IMG_MAX} from '#/lib/constants' +import {formatToFileExt} from '#/lib/media/image-formats' import {type PickerImage} from './picker.shared' import {type Dimensions} from './types' -import {blobToDataUri, convertCdnPreset, getDataUriSize} from './util' +import {blobToDataUri, getDataUriSize, getDownloadImageUri} from './util' import {mimeToExt} from './video/util' export async function compressIfNeeded( @@ -52,14 +53,20 @@ export async function shareImageModal(_opts: {uri: string}) { } /** - * Saves an image to the user's device. Uses the CDN's `download` preset - * which serves a JPEG with `Content-Disposition: attachment`. On web this - * triggers a browser download via a temporary anchor — no fetch needed. + * Saves an image to the user's device. Uses the CDN's `download` preset with the + * chosen format suffix. On web this triggers a browser download via a temporary + * anchor — no fetch needed. */ -export async function saveImageToMediaLibrary({uri}: {uri: string}) { - const downloadUri = convertCdnPreset(uri, 'download') +export async function saveImageToMediaLibrary({ + uri, + format = 'jpeg', +}: { + uri: string + format?: string +}) { + const downloadUri = getDownloadImageUri(uri, format) const segments = downloadUri.split('/') - const filename = `bluesky-${segments.at(-1) ?? 'image'}.jpg` + const filename = `bluesky-${segments.at(-1) ?? 'image'}.${formatToFileExt(format)}` downloadUrl(downloadUri, filename) } diff --git a/src/lib/media/save-image.ios.ts b/src/lib/media/save-image.ios.ts index 7bd270de8..3d62a0c2d 100644 --- a/src/lib/media/save-image.ios.ts +++ b/src/lib/media/save-image.ios.ts @@ -2,6 +2,7 @@ import {useCallback} from 'react' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' +import {useDownloadFormat} from '#/state/preferences/download-format' import * as Toast from '#/components/Toast' import {IS_NATIVE} from '#/env' import {saveImageToMediaLibrary} from './manip' @@ -14,6 +15,7 @@ import {saveImageToMediaLibrary} from './manip' */ export function useSaveImageToMediaLibrary() { const {_} = useLingui() + const downloadFormat = useDownloadFormat() return useCallback( async (uri: string) => { if (!IS_NATIVE) { @@ -21,12 +23,15 @@ export function useSaveImageToMediaLibrary() { } try { - await saveImageToMediaLibrary({uri}) + await saveImageToMediaLibrary({ + uri, + format: downloadFormat ?? 'jpeg', + }) Toast.show(_(msg`Image saved`)) } catch (e: any) { Toast.show(_(msg`Failed to save image: ${String(e)}`), {type: 'error'}) } }, - [_], + [downloadFormat, _], ) } diff --git a/src/lib/media/save-image.ts b/src/lib/media/save-image.ts index bf48d8e68..d2753c835 100644 --- a/src/lib/media/save-image.ts +++ b/src/lib/media/save-image.ts @@ -3,6 +3,7 @@ import * as MediaLibrary from 'expo-media-library' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' +import {useDownloadFormat} from '#/state/preferences/download-format' import * as Toast from '#/components/Toast' import {IS_NATIVE} from '#/env' import {saveImageToMediaLibrary} from './manip' @@ -12,6 +13,7 @@ import {saveImageToMediaLibrary} from './manip' */ export function useSaveImageToMediaLibrary() { const {_} = useLingui() + const downloadFormat = useDownloadFormat() const [permissionResponse, requestPermission, getPermission] = MediaLibrary.usePermissions({ granularPermissions: ['photo'], @@ -24,7 +26,10 @@ export function useSaveImageToMediaLibrary() { async function save() { try { - await saveImageToMediaLibrary({uri}) + await saveImageToMediaLibrary({ + uri, + format: downloadFormat ?? 'jpeg', + }) Toast.show(_(msg`Image saved`)) } catch (e: any) { @@ -63,6 +68,6 @@ export function useSaveImageToMediaLibrary() { } } }, - [permissionResponse, requestPermission, getPermission, _], + [permissionResponse, requestPermission, getPermission, downloadFormat, _], ) } diff --git a/src/lib/media/util.ts b/src/lib/media/util.ts index da0b306d4..ab08aece7 100644 --- a/src/lib/media/util.ts +++ b/src/lib/media/util.ts @@ -47,3 +47,14 @@ const IMGPROXY_PRESET_RE = export function convertCdnPreset(uri: string, preset: ImgproxyPreset): string { return uri.replace(IMGPROXY_PRESET_RE, `$1${preset}$3`) } + +export function modifyImageFormat(uri: string, format: string) { + const atPosition = uri.lastIndexOf('@') + return atPosition === -1 + ? `${uri}@${format}` + : uri.slice(0, atPosition + 1) + format +} + +export function getDownloadImageUri(uri: string, format: string) { + return modifyImageFormat(convertCdnPreset(uri, 'download'), format) +} diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 7332da95a..0b76eab21 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -16,11 +16,12 @@ import {type NavigationProp} from '#/lib/routes/types' import {type Shadow} from '#/state/cache/types' import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars' import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' -import {useHighQualityImages} from '#/state/preferences/high-quality-images' +import {useFullsizeFormat} from '#/state/preferences/fullsize-format' import { applyImageTransforms, useImageCdnHost, } from '#/state/preferences/image-cdn-host' +import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' import {useSession} from '#/state/session' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {UserAvatar} from '#/view/com/util/UserAvatar' @@ -65,7 +66,8 @@ let ProfileHeaderShell = ({ const {top: topInset} = useSafeAreaInsets() const playHaptic = useHaptics() const liveStatusControl = useDialogControl() - const highQualityImages = useHighQualityImages() + const fullsizeFormat = useFullsizeFormat() + const thumbnailFormat = useThumbnailFormat() const imageCdnHost = useImageCdnHost() const enableSquareAvatars = useEnableSquareAvatars() const enableSquareButtons = useEnableSquareButtons() @@ -90,10 +92,13 @@ let ProfileHeaderShell = ({ openLightbox({ images: [ { - uri: applyImageTransforms(uri, {imageCdnHost, highQualityImages}), + uri: applyImageTransforms(uri, { + imageCdnHost, + format: fullsizeFormat, + }), thumbUri: applyImageTransforms(uri, { imageCdnHost, - highQualityImages, + format: thumbnailFormat, }), thumbRect: null, thumbRef, @@ -121,7 +126,13 @@ let ProfileHeaderShell = ({ index: 0, }) }, - [openLightbox, imageCdnHost, highQualityImages, enableSquareAvatars], + [ + openLightbox, + imageCdnHost, + fullsizeFormat, + thumbnailFormat, + enableSquareAvatars, + ], ) const isMe = useMemo( diff --git a/src/screens/Settings/RunesSettings/DisplaySettings.tsx b/src/screens/Settings/RunesSettings/DisplaySettings.tsx index e90244aa2..45c35992c 100644 --- a/src/screens/Settings/RunesSettings/DisplaySettings.tsx +++ b/src/screens/Settings/RunesSettings/DisplaySettings.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native' import {Trans, useLingui} from '@lingui/react/macro' import {usePalette} from '#/lib/hooks/usePalette' +import {IMAGE_FORMATS} from '#/lib/media/image-formats' import {dynamicActivate} from '#/locale/i18n' import {dynamicActivate as dynamicActivateWeb} from '#/locale/i18n.web' import {type AppLanguage} from '#/locale/languages' @@ -14,9 +15,17 @@ import { import {useAutoCompactAccountSwitcher} from '#/state/preferences/auto-compact-account-switcher' import {useCompactAccountSwitcher} from '#/state/preferences/compact-account-switcher' import { - useHighQualityImages, - useSetHighQualityImages, -} from '#/state/preferences/high-quality-images' + useDownloadFormat, + useSetDownloadFormat, +} from '#/state/preferences/download-format' +import { + useFullsizeFormat, + useSetFullsizeFormat, +} from '#/state/preferences/fullsize-format' +import { + useLoadAsPngs, + useSetLoadAsPngs, +} from '#/state/preferences/load-small-pngs' import { usePostReplacement, useSetPostReplacement, @@ -25,9 +34,12 @@ import { useSetShowViaClient, useShowViaClient, } from '#/state/preferences/show-via-client' +import { + useSetThumbnailFormat, + useThumbnailFormat, +} from '#/state/preferences/thumbnail-format' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a} from '#/alf' -import {Admonition} from '#/components/Admonition' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as Toggle from '#/components/forms/Toggle' @@ -36,6 +48,7 @@ import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Ima import {Pencil_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' +import * as Select from '#/components/Select' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' import {ItemTextWithSubtitle} from '../NotificationSettings/components/ItemTextWithSubtitle' @@ -47,12 +60,18 @@ export function RunesDisplaySettingsScreen() { const alsoLikedFeedEnabled = useAlsoLikedFeedEnabled() const alsoLikedCollapseByDefault = useAlsoLikedCollapseByDefault() - const highQualityImages = useHighQualityImages() - const setHighQualityImages = useSetHighQualityImages() - const showViaClient = useShowViaClient() const setShowViaClient = useSetShowViaClient() + const thumbnailFormat = useThumbnailFormat() ?? 'webp' + const setThumbnailFormat = useSetThumbnailFormat() + const fullsizeFormat = useFullsizeFormat() ?? 'webp' + const setFullsizeFormat = useSetFullsizeFormat() + const downloadFormat = useDownloadFormat() ?? 'jpeg' + const setDownloadFormat = useSetDownloadFormat() + const loadAsPngs = useLoadAsPngs() + const setLoadAsPngs = useSetLoadAsPngs() + const setPostReplacementDialogControl = Dialog.useDialogControl() return ( @@ -82,27 +101,6 @@ export function RunesDisplaySettingsScreen() { subtitleText={} /> - setHighQualityImages(value)}> - - - - Display images in higher quality - - - - - - - - Images will be served as PNG instead of WEBP. Images will take - longer to load and use more bandwidth. - - - + + + + + + Images + + + + Thumbnail format + + setThumbnailFormat(value)}> + + + + + ( + + + {label} + + )} + items={IMAGE_FORMATS} + /> + + + + + Full-size format + + setFullsizeFormat(value)}> + + + + + ( + + + {label} + + )} + items={IMAGE_FORMATS} + /> + + + + + Download format + + setDownloadFormat(value)}> + + + + + ( + + + {label} + + )} + items={IMAGE_FORMATS} + /> + + + setLoadAsPngs(value)} + style={[a.w_full, a.mt_xs]}> + + + Load especially small images as PNGs + + + ) } diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 9d0f513c5..f5d069533 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -213,7 +213,12 @@ const schema = z.object({ trusted: z.array(z.string()), }) .optional(), + /** @deprecated Use thumbnailFormat and fullsizeFormat instead */ highQualityImages: z.boolean().optional(), + thumbnailFormat: z.string().optional(), + fullsizeFormat: z.string().optional(), + downloadFormat: z.string().optional(), + loadAsPngs: z.boolean().optional(), imageCdnHost: z.string().optional(), plcDirectory: z.string().optional(), hideUnreplyablePosts: z.boolean().optional(), @@ -357,6 +362,10 @@ export const defaults: Schema = { trusted: [], }, highQualityImages: false, + thumbnailFormat: 'webp', + fullsizeFormat: 'webp', + downloadFormat: 'jpeg', + loadAsPngs: true, imageCdnHost: 'https://cdn.bsky.app', plcDirectory: 'https://plc.directory', hideUnreplyablePosts: false, @@ -403,16 +412,24 @@ export function tryParse(rawData: string): Schema | undefined { } const parsed = schema.safeParse(objData) if (parsed.success) { - const data = parsed.data + let data = parsed.data if ( (objData as {noAppLabelers?: boolean}).noAppLabelers && !data.ignoredAppLabelers?.length ) { - return { + data = { ...data, ignoredAppLabelers: [BSKY_LABELER_DID], } } + if (data.highQualityImages) { + data = { + ...data, + thumbnailFormat: data.thumbnailFormat ?? 'png', + fullsizeFormat: data.fullsizeFormat ?? 'png', + loadAsPngs: data.loadAsPngs ?? true, + } + } return data } else { const errors = diff --git a/src/state/preferences/download-format.tsx b/src/state/preferences/download-format.tsx new file mode 100644 index 000000000..a847bd6ae --- /dev/null +++ b/src/state/preferences/download-format.tsx @@ -0,0 +1,54 @@ +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['downloadFormat'] +type SetContext = (v: persisted.Schema['downloadFormat']) => void + +const stateContext = createContext( + persisted.defaults.downloadFormat, +) +const setContext = createContext( + (_: persisted.Schema['downloadFormat']) => {}, +) + +export function Provider({children}: PropsWithChildren<{}>) { + const [state, setState] = useState(persisted.get('downloadFormat')) + + const setStateWrapped = useCallback( + (downloadFormat: persisted.Schema['downloadFormat']) => { + setState(downloadFormat) + persisted.write('downloadFormat', downloadFormat) + }, + [setState], + ) + + useEffect(() => { + return persisted.onUpdate('downloadFormat', nextDownloadFormat => { + setState(nextDownloadFormat) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useDownloadFormat() { + return useContext(stateContext) +} + +export function useSetDownloadFormat() { + return useContext(setContext) +} diff --git a/src/state/preferences/fullsize-format.tsx b/src/state/preferences/fullsize-format.tsx new file mode 100644 index 000000000..0ff52860c --- /dev/null +++ b/src/state/preferences/fullsize-format.tsx @@ -0,0 +1,54 @@ +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['fullsizeFormat'] +type SetContext = (v: persisted.Schema['fullsizeFormat']) => void + +const stateContext = createContext( + persisted.defaults.fullsizeFormat, +) +const setContext = createContext( + (_: persisted.Schema['fullsizeFormat']) => {}, +) + +export function Provider({children}: PropsWithChildren<{}>) { + const [state, setState] = useState(persisted.get('fullsizeFormat')) + + const setStateWrapped = useCallback( + (fullsizeFormat: persisted.Schema['fullsizeFormat']) => { + setState(fullsizeFormat) + persisted.write('fullsizeFormat', fullsizeFormat) + }, + [setState], + ) + + useEffect(() => { + return persisted.onUpdate('fullsizeFormat', nextFullsizeFormat => { + setState(nextFullsizeFormat) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useFullsizeFormat() { + return useContext(stateContext) +} + +export function useSetFullsizeFormat() { + return useContext(setContext) +} diff --git a/src/state/preferences/high-quality-images.tsx b/src/state/preferences/high-quality-images.tsx deleted file mode 100644 index 0eff6dbfe..000000000 --- a/src/state/preferences/high-quality-images.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { - createContext, - type PropsWithChildren, - useCallback, - useContext, - useEffect, - useState, -} from 'react' - -import * as persisted from '#/state/persisted' - -type StateContext = persisted.Schema['highQualityImages'] -type SetContext = (v: persisted.Schema['highQualityImages']) => void - -const stateContext = createContext( - persisted.defaults.highQualityImages, -) -const setContext = createContext( - (_: persisted.Schema['highQualityImages']) => {}, -) - -export function Provider({children}: PropsWithChildren<{}>) { - const [state, setState] = useState(persisted.get('highQualityImages')) - - const setStateWrapped = useCallback( - (highQualityImages: persisted.Schema['highQualityImages']) => { - setState(highQualityImages) - persisted.write('highQualityImages', highQualityImages) - }, - [setState], - ) - - useEffect(() => { - return persisted.onUpdate('highQualityImages', nextHighQualityImages => { - setState(nextHighQualityImages) - }) - }, [setStateWrapped]) - - return ( - - - {children} - - - ) -} - -export function useHighQualityImages() { - return useContext(stateContext) -} - -export function useSetHighQualityImages() { - return useContext(setContext) -} - -// This is a little weird to have here imo but it works I guess -function modifyHighQualityImage(src: string) { - try { - const url = new URL(src) - if (url.hostname === 'cdn.bsky.app') { - // bluesky does not have this in urls anymore, but some forks like blacksky still do this so we need to still check for this - if (url.pathname.endsWith('@jpeg')) - url.pathname = url.pathname.replace(/@jpeg$/, '@png') - else url.pathname = url.pathname += '@png' - return url.toString() - } - } catch { - // ignored, in case the URL is somehow malformed - } - - return null -} - -// Like `hackModifyThumbnailPath`, it's easier to just pipe the src into a function like this -export function maybeModifyHighQualityImage(src: string, isEnabled?: boolean) { - if (isEnabled) { - return modifyHighQualityImage(src) ?? src - } else { - return src - } -} diff --git a/src/state/preferences/image-cdn-host.tsx b/src/state/preferences/image-cdn-host.tsx index 22d22440d..909a34092 100644 --- a/src/state/preferences/image-cdn-host.tsx +++ b/src/state/preferences/image-cdn-host.tsx @@ -99,26 +99,19 @@ export function maybeModifyImageCdnHost(src: string, imageCdnHost?: string) { return modifyImageCdnHost(src, nextOrigin) ?? src } +import {modifyImageFormat} from '#/lib/media/util' + /** - * Combined image transformation pipeline: applies high-quality image format - * transformation first (on original CDN), then applies CDN host rewrite. + * Combined image transformation pipeline: applies image format, then CDN host + * rewrite. */ export function applyImageTransforms( src: string, options: { imageCdnHost?: string - highQualityImages?: boolean + format?: string }, ) { - // Import is deferred to avoid circular dependency - const {maybeModifyHighQualityImage} = - require('./high-quality-images') as typeof import('./high-quality-images') - - // First apply quality transformation on original CDN - const withQuality = maybeModifyHighQualityImage( - src, - options.highQualityImages, - ) - // Then apply CDN host replacement - return maybeModifyImageCdnHost(withQuality, options.imageCdnHost) + const withFormat = modifyImageFormat(src, options.format ?? 'webp') + return maybeModifyImageCdnHost(withFormat, options.imageCdnHost) } diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index 344428a31..77aebef60 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -28,24 +28,26 @@ import {Provider as DisableTopOfFeedButtonProvider} from './disable-top-of-feed- import {Provider as DisableVerifyEmailReminderProvider} from './disable-verify-email-reminder' import {Provider as DisableViaRepostNotificationProvider} from './disable-via-repost-notification' import {Provider as DiscoverContextEnabledProvider} from './discover-context-enabled' +import {Provider as DownloadFormatProvider} from './download-format' import {Provider as EnableSquareAvatarsProvider} from './enable-square-avatars' import {Provider as EnableSquareButtonsProvider} from './enable-square-buttons' import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs' import {Provider as ExternalShareButtonsProvider} from './external-share-buttons' import {Provider as FaviconServiceProvider} from './favicon-service' +import {Provider as FullsizeFormatProvider} from './fullsize-format' import {Provider as GoLinksProvider} from './go-links-enabled' import {Provider as HiddenPostsProvider} from './hidden-posts' import {Provider as HideFeedsPromoTabProvider} from './hide-feeds-promo-tab' import {Provider as HideScaryFollowButtonsProvider} from './hide-scary-follow-buttons.tsx' import {Provider as HideSimilarAccountsRecommProvider} from './hide-similar-accounts-recommendations' import {Provider as HideUnreplyablePostsProvider} from './hide-unreplyable-posts' -import {Provider as HighQualityImagesProvider} from './high-quality-images' import {Provider as IgnoredAppLabelersProvider} from './ignored-app-labelers' import {Provider as ImageCdnHostProvider} from './image-cdn-host' import {Provider as InAppBrowserProvider} from './in-app-browser' import {Provider as KawaiiProvider} from './kawaii' import {Provider as LanguagesProvider} from './languages' import {Provider as LargeAltBadgeProvider} from './large-alt-badge' +import {Provider as LoadSmallPNGsProvider} from './load-small-pngs' import {Provider as NoDiscoverProvider} from './no-discover-fallback' import {Provider as OmitViaFieldProvider} from './omit-via-field' import {Provider as OpenRouterProvider} from './openrouter' @@ -61,6 +63,7 @@ import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle' import {Provider as ShowLinkInHandleOnlyOnWorkingLinksProvider} from './show-link-in-handle-only-on-working-links' import {Provider as ShowViaClientProvider} from './show-via-client' import {Provider as SubtitlesProvider} from './subtitles' +import {Provider as ThumbnailFormatProvider} from './thumbnail-format' import {Provider as TranslationServicePreferenceProvider} from './translation-service-preference' import {Provider as TrendingSettingsProvider} from './trending' import {Provider as UseHandleInLinksProvider} from './use-handle-in-links' @@ -168,99 +171,105 @@ export function Provider({children}: PropsWithChildren<{}>) { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { - children - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + children + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/state/preferences/load-small-pngs.tsx b/src/state/preferences/load-small-pngs.tsx new file mode 100644 index 000000000..8b44cb7b9 --- /dev/null +++ b/src/state/preferences/load-small-pngs.tsx @@ -0,0 +1,52 @@ +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['loadAsPngs'] +type SetContext = (v: persisted.Schema['loadAsPngs']) => void + +const stateContext = createContext(persisted.defaults.loadAsPngs) +const setContext = createContext( + (_: persisted.Schema['loadAsPngs']) => {}, +) + +export function Provider({children}: PropsWithChildren<{}>) { + const [state, setState] = useState(persisted.get('loadAsPngs')) + + const setStateWrapped = useCallback( + (loadAsPngs: persisted.Schema['loadAsPngs']) => { + setState(loadAsPngs) + persisted.write('loadAsPngs', loadAsPngs) + }, + [setState], + ) + + useEffect(() => { + return persisted.onUpdate('loadAsPngs', nextLoadAsPngs => { + setState(nextLoadAsPngs) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useLoadAsPngs() { + return useContext(stateContext) +} + +export function useSetLoadAsPngs() { + return useContext(setContext) +} diff --git a/src/state/preferences/settings-sync.tsx b/src/state/preferences/settings-sync.tsx index 7d8ec8303..75c78b6e6 100644 --- a/src/state/preferences/settings-sync.tsx +++ b/src/state/preferences/settings-sync.tsx @@ -74,7 +74,10 @@ export const SYNCED_PREFS_KEYS = [ 'disableVerifyEmailReminder', 'showViaClient', 'deerVerification', - 'highQualityImages', + 'thumbnailFormat', + 'fullsizeFormat', + 'downloadFormat', + 'loadAsPngs', 'imageCdnHost', 'plcDirectory', 'hideUnreplyablePosts', diff --git a/src/state/preferences/thumbnail-format.tsx b/src/state/preferences/thumbnail-format.tsx new file mode 100644 index 000000000..ca2bef851 --- /dev/null +++ b/src/state/preferences/thumbnail-format.tsx @@ -0,0 +1,54 @@ +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['thumbnailFormat'] +type SetContext = (v: persisted.Schema['thumbnailFormat']) => void + +const stateContext = createContext( + persisted.defaults.thumbnailFormat, +) +const setContext = createContext( + (_: persisted.Schema['thumbnailFormat']) => {}, +) + +export function Provider({children}: PropsWithChildren<{}>) { + const [state, setState] = useState(persisted.get('thumbnailFormat')) + + const setStateWrapped = useCallback( + (thumbnailFormat: persisted.Schema['thumbnailFormat']) => { + setState(thumbnailFormat) + persisted.write('thumbnailFormat', thumbnailFormat) + }, + [setState], + ) + + useEffect(() => { + return persisted.onUpdate('thumbnailFormat', nextThumbnailFormat => { + setState(nextThumbnailFormat) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useThumbnailFormat() { + return useContext(stateContext) +} + +export function useSetThumbnailFormat() { + return useContext(setContext) +} diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index f14a33689..55be5d865 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -38,11 +38,11 @@ import { createComposerImage, } from '#/state/gallery' import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars' -import {useHighQualityImages} from '#/state/preferences/high-quality-images' import { applyImageTransforms, useImageCdnHost, } from '#/state/preferences/image-cdn-host' +import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog' import {atoms as a, tokens, useTheme} from '#/alf' @@ -253,7 +253,7 @@ let UserAvatar = ({ const finalShape = overrideShape ?? (type === 'user' ? avishapeforce : 'square') - const highQualityImages = useHighQualityImages() + const thumbnailFormat = useThumbnailFormat() const imageCdnHost = useImageCdnHost() const aviStyle = useMemo(() => { @@ -343,7 +343,7 @@ let UserAvatar = ({ source={{ uri: applyImageTransforms( hackModifyThumbnailPath(avatar, size < 90), - {imageCdnHost, highQualityImages}, + {imageCdnHost, format: thumbnailFormat}, ), }} blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} @@ -357,7 +357,7 @@ let UserAvatar = ({ source={{ uri: applyImageTransforms( hackModifyThumbnailPath(avatar, size < 90), - {imageCdnHost, highQualityImages}, + {imageCdnHost, format: thumbnailFormat}, ), }} blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} @@ -398,7 +398,7 @@ let EditableUserAvatar = ({ const editImageDialogControl = useDialogControl() const sheetWrapper = useSheetWrapper() - const highQualityImages = useHighQualityImages() + const thumbnailFormat = useThumbnailFormat() const imageCdnHost = useImageCdnHost() const enableSquareAvatars = useEnableSquareAvatars() @@ -519,7 +519,7 @@ let EditableUserAvatar = ({ source={{ uri: applyImageTransforms(avatar, { imageCdnHost, - highQualityImages, + format: thumbnailFormat, }), }} accessibilityRole="image" diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index cb8cf6ef5..8edb51aec 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -20,11 +20,12 @@ import { compressImage, createComposerImage, } from '#/state/gallery' -import {useHighQualityImages} from '#/state/preferences/high-quality-images' +import {useFullsizeFormat} from '#/state/preferences/fullsize-format' import { applyImageTransforms, useImageCdnHost, } from '#/state/preferences/image-cdn-host' +import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog' import {EventStopper} from '#/view/com/util/EventStopper' import {atoms as a, tokens, useTheme} from '#/alf' @@ -57,7 +58,8 @@ export function UserBanner({ const sheetWrapper = useSheetWrapper() const [rawImage, setRawImage] = useState() const editImageDialogControl = useDialogControl() - const highQualityImages = useHighQualityImages() + const fullsizeFormat = useFullsizeFormat() + const thumbnailFormat = useThumbnailFormat() const imageCdnHost = useImageCdnHost() const onOpenCamera = useCallback(async () => { @@ -139,7 +141,7 @@ export function UserBanner({ source={{ uri: applyImageTransforms(banner, { imageCdnHost, - highQualityImages, + format: thumbnailFormat, }), }} accessible={true} @@ -228,7 +230,10 @@ export function UserBanner({ style={[styles.bannerImage, t.atoms.bg_contrast_25]} contentFit="cover" source={{ - uri: applyImageTransforms(banner, {imageCdnHost, highQualityImages}), + uri: applyImageTransforms(banner, { + imageCdnHost, + format: fullsizeFormat, + }), }} blurRadius={moderation?.blur ? 100 : 0} accessible={true} -- 2.51.2