deer social fork for personal usage. but you might see a use idk. github mirror
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

better quality image loading

ayla fb1a2ac5 1c284cb6

+397 -188
+4 -9
src/components/MediaPreview.tsx
··· 4 4 import {Trans} from '@lingui/macro' 5 5 import type React from 'react' 6 6 7 + import {modifyImageFormat} from '#/lib/media/util' 7 8 import {isTenorGifUri} from '#/lib/strings/embed-player' 8 - import { 9 - maybeModifyHighQualityImage, 10 - useHighQualityImages, 11 - } from '#/state/preferences/high-quality-images' 9 + import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' 12 10 import {atoms as a, useTheme} from '#/alf' 13 11 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 14 12 import {Text} from '#/components/Typography' ··· 25 23 embed: AppBskyFeedDefs.PostView['embed'] 26 24 style?: StyleProp<ViewStyle> 27 25 }) { 28 - const highQualityImages = useHighQualityImages() 26 + const imageFormat = useThumbnailFormat() 29 27 const e = bsky.post.parseEmbed(embed) 30 28 31 29 if (!e) return null ··· 36 34 {e.view.images.map(image => ( 37 35 <ImageItem 38 36 key={image.thumb} 39 - thumbnail={maybeModifyHighQualityImage( 40 - image.thumb, 41 - highQualityImages, 42 - )} 37 + thumbnail={modifyImageFormat(image.thumb, imageFormat)} 43 38 alt={image.alt} 44 39 /> 45 40 ))}
+56 -12
src/components/Post/Embed/ImageEmbed.tsx
··· 8 8 } from 'react-native-reanimated' 9 9 import {Image} from 'expo-image' 10 10 11 + import {PNG_IMG_MAX_BYTE, PNG_IMG_MAX_SIZE} from '#/lib/constants' 12 + import {modifyImageFormat} from '#/lib/media/util' 11 13 import {useLightboxControls} from '#/state/lightbox' 12 - import { 13 - maybeModifyHighQualityImage, 14 - useHighQualityImages, 15 - } from '#/state/preferences/high-quality-images' 14 + import {useFullsizeFormat} from '#/state/preferences/fullsize-format' 15 + import {useLoadAsPngs} from '#/state/preferences/load-small-pngs' 16 + import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' 16 17 import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types' 17 18 import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage' 18 19 import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid' 19 20 import {atoms as a} from '#/alf' 20 21 import {PostEmbedViewContext} from '#/components/Post/Embed/types' 21 - import {type EmbedType} from '#/types/bsky/post' 22 + import { 23 + type BetterImage, 24 + type BetterImages, 25 + type EmbedType, 26 + } from '#/types/bsky/post' 22 27 import {type CommonProps} from './types' 23 28 24 29 export function ImageEmbed({ 25 30 embed, 31 + recordEmbed, 26 32 ...rest 27 33 }: CommonProps & { 28 34 embed: EmbedType<'images'> 35 + recordEmbed?: BetterImages | {media?: BetterImages} 29 36 }) { 30 37 const {openLightbox} = useLightboxControls() 31 - const highQualityImages = useHighQualityImages() 38 + const fullsizeFormat = useFullsizeFormat() 39 + const thumbnailFormat = useThumbnailFormat() 40 + const loadAsPngs = useLoadAsPngs() 32 41 const {images} = embed.view 42 + const recordImages = (recordEmbed && 43 + (('media' in recordEmbed && recordEmbed?.media?.images) || 44 + ('images' in recordEmbed && recordEmbed?.images))) || [{} as BetterImage] 33 45 34 46 if (images.length > 0) { 35 - const items = images.map(img => ({ 36 - uri: maybeModifyHighQualityImage(img.fullsize, highQualityImages), 37 - thumbUri: maybeModifyHighQualityImage(img.thumb, highQualityImages), 38 - alt: img.alt, 39 - dimensions: img.aspectRatio ?? null, 40 - })) 47 + const items = images.map((img, index) => { 48 + const recordImage = recordImages[index] ?? [] 49 + 50 + const pngSized = 51 + (loadAsPngs 52 + ? recordImage.size 53 + ? recordImage.quality === 100 && 54 + recordImage.size <= PNG_IMG_MAX_BYTE 55 + : img.aspectRatio && 56 + img.aspectRatio.width <= PNG_IMG_MAX_SIZE && 57 + img.aspectRatio.height <= PNG_IMG_MAX_SIZE 58 + : false) || false 59 + 60 + const fullsizeUri = modifyImageFormat( 61 + img.fullsize, 62 + pngSized || 63 + (loadAsPngs && 64 + recordImage.quality === 100 && 65 + recordImage.mime !== 'image/jpeg') 66 + ? 'png' 67 + : fullsizeFormat, 68 + ) 69 + 70 + // i'm aware of how ridiculous this looks 71 + // but i think this is the easiest way of doing this 72 + // and it doesn't look thaaaat bad but yeah 73 + img.fullsize = fullsizeUri 74 + img.thumb = pngSized 75 + ? fullsizeUri 76 + : modifyImageFormat(img.thumb, thumbnailFormat) 77 + 78 + return { 79 + uri: img.fullsize, 80 + thumbUri: img.thumb, 81 + alt: img.alt, 82 + dimensions: img.aspectRatio ?? null, 83 + } 84 + }) 41 85 const _openLightbox = ( 42 86 index: number, 43 87 thumbRects: (MeasuredDimensions | null)[],
+11 -4
src/components/Post/Embed/index.tsx
··· 29 29 import {SubtleWebHover} from '#/components/SubtleWebHover' 30 30 import * as bsky from '#/types/bsky' 31 31 import { 32 + type BetterImages, 32 33 type Embed as TEmbed, 33 34 type EmbedType, 34 35 parseEmbed, ··· 48 49 49 50 export {PostEmbedViewContext, QuoteEmbedViewContext} from './types' 50 51 51 - export function Embed({embed: rawEmbed, ...rest}: EmbedProps) { 52 + export function Embed({embed: rawEmbed, recordEmbed, ...rest}: EmbedProps) { 52 53 const embed = parseEmbed(rawEmbed) 53 54 54 55 switch (embed.type) { 55 56 case 'images': 56 57 case 'link': 57 58 case 'video': { 58 - return <MediaEmbed embed={embed} {...rest} /> 59 + return <MediaEmbed embed={embed} recordEmbed={recordEmbed} {...rest} /> 59 60 } 60 61 case 'feed': 61 62 case 'list': ··· 70 71 case 'post_with_media': { 71 72 return ( 72 73 <View style={rest.style}> 73 - <MediaEmbed embed={embed.media} {...rest} /> 74 + <MediaEmbed embed={embed.media} recordEmbed={recordEmbed} {...rest} /> 74 75 <RecordEmbed embed={embed.view} {...rest} /> 75 76 </View> 76 77 ) ··· 83 84 84 85 function MediaEmbed({ 85 86 embed, 87 + recordEmbed, 86 88 ...rest 87 89 }: CommonProps & { 88 90 embed: TEmbed 91 + recordEmbed?: AppBskyFeedPost.Record['embed'] 89 92 }) { 90 93 switch (embed.type) { 91 94 case 'images': { ··· 93 96 <ContentHider 94 97 modui={rest.moderation?.ui('contentMedia')} 95 98 activeStyle={[a.mt_sm]}> 96 - <ImageEmbed embed={embed} {...rest} /> 99 + <ImageEmbed 100 + embed={embed} 101 + recordEmbed={recordEmbed as BetterImages} 102 + {...rest} 103 + /> 97 104 </ContentHider> 98 105 ) 99 106 }
+6 -1
src/components/Post/Embed/types.ts
··· 1 1 import {type StyleProp, type ViewStyle} from 'react-native' 2 - import {type AppBskyFeedDefs, type ModerationDecision} from '@atproto/api' 2 + import { 3 + type AppBskyFeedDefs, 4 + type AppBskyFeedPost, 5 + type ModerationDecision, 6 + } from '@atproto/api' 3 7 4 8 export enum PostEmbedViewContext { 5 9 ThreadHighlighted = 'ThreadHighlighted', ··· 22 26 23 27 export type EmbedProps = CommonProps & { 24 28 embed?: AppBskyFeedDefs.PostView['embed'] 29 + recordEmbed?: AppBskyFeedPost.Record['embed'] 25 30 }
+3
src/lib/constants.ts
··· 88 88 size: 1000000, 89 89 } 90 90 91 + export const PNG_IMG_MAX_BYTE = 100000 92 + export const PNG_IMG_MAX_SIZE = 300 93 + 91 94 export const STAGING_LINK_META_PROXY = 92 95 'https://cardyb.staging.bsky.dev/v1/extract?url=' 93 96
+8
src/lib/media/image-formats.ts
··· 1 + export const IMAGE_FORMATS = [ 2 + {label: 'AVIF', value: 'avif'}, 3 + {label: 'WebP', value: 'webp'}, 4 + {label: 'JPEG', value: 'jpeg'}, 5 + {label: 'PNG', value: 'png'}, 6 + {label: 'GIF', value: 'gif'}, 7 + {label: 'BMP', value: 'bmp'}, 8 + ]
+5
src/lib/media/util.ts
··· 26 26 reader.readAsDataURL(blob) 27 27 }) 28 28 } 29 + 30 + export function modifyImageFormat(uri: string, format: string) { 31 + const sliced = uri.slice(0, -4) 32 + return sliced.at(-1) === '@' ? sliced + format : uri 33 + }
+1
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 413 413 <View style={[a.py_xs]}> 414 414 <Embed 415 415 embed={post.embed} 416 + recordEmbed={record.embed} 416 417 moderation={moderation} 417 418 viewContext={PostEmbedViewContext.ThreadHighlighted} 418 419 onOpen={onOpenEmbed}
+1
src/screens/PostThread/components/ThreadItemPost.tsx
··· 322 322 <View style={[a.pb_xs]}> 323 323 <Embed 324 324 embed={post.embed} 325 + recordEmbed={record.embed} 325 326 moderation={moderation} 326 327 viewContext={PostEmbedViewContext.Feed} 327 328 />
+1
src/screens/PostThread/components/ThreadItemTreePost.tsx
··· 362 362 <View style={[a.pb_xs]}> 363 363 <Embed 364 364 embed={post.embed} 365 + recordEmbed={record.embed} 365 366 moderation={moderation} 366 367 viewContext={PostEmbedViewContext.Feed} 367 368 />
+89 -25
src/screens/Settings/DeerSettings.tsx
··· 6 6 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 7 7 8 8 import {usePalette} from '#/lib/hooks/usePalette' 9 + import {IMAGE_FORMATS} from '#/lib/media/image-formats' 9 10 import {type CommonNavigatorParams} from '#/lib/routes/types' 10 11 import {type Gate} from '#/lib/statsig/gates' 11 12 import { ··· 58 59 useSetDisableViaRepostNotification, 59 60 } from '#/state/preferences/disable-via-repost-notification' 60 61 import { 62 + useFullsizeFormat, 63 + useSetFullsizeFormat, 64 + } from '#/state/preferences/fullsize-format' 65 + import { 61 66 useHideFeedsPromoTab, 62 67 useSetHideFeedsPromoTab, 63 68 } from '#/state/preferences/hide-feeds-promo-tab' ··· 66 71 useSetHideSimilarAccountsRecomm, 67 72 } from '#/state/preferences/hide-similar-accounts-recommendations' 68 73 import { 69 - useHighQualityImages, 70 - useSetHighQualityImages, 71 - } from '#/state/preferences/high-quality-images' 74 + useLoadAsPngs, 75 + useSetLoadAsPngs, 76 + } from '#/state/preferences/load-small-pngs' 72 77 import {useModerationOpts} from '#/state/preferences/moderation-opts' 73 78 import { 74 79 useNoAppLabelers, ··· 85 90 import { 86 91 useSetShowLinkInHandle, 87 92 useShowLinkInHandle, 88 - } from '#/state/preferences/show-link-in-handle.tsx' 93 + } from '#/state/preferences/show-link-in-handle' 94 + import { 95 + useSetThumbnailFormat, 96 + useThumbnailFormat, 97 + } from '#/state/preferences/thumbnail-format' 89 98 import {useProfilesQuery} from '#/state/queries/profile' 90 99 import * as SettingsList from '#/screens/Settings/components/SettingsList' 91 100 import {atoms as a, useBreakpoints} from '#/alf' ··· 101 110 import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' 102 111 import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' 103 112 import * as Layout from '#/components/Layout' 113 + import * as Select from '#/components/Select' 104 114 import {Text} from '#/components/Typography' 105 115 import {SearchProfileCard} from '../Search/components/SearchProfileCard' 106 116 ··· 249 259 const noDiscoverFallback = useNoDiscoverFallback() 250 260 const setNoDiscoverFallback = useSetNoDiscoverFallback() 251 261 252 - const highQualityImages = useHighQualityImages() 253 - const setHighQualityImages = useSetHighQualityImages() 254 - 255 262 const hideFeedsPromoTab = useHideFeedsPromoTab() 256 263 const setHideFeedsPromoTab = useSetHideFeedsPromoTab() 257 264 ··· 281 288 282 289 const setTrustedVerifiersDialogControl = Dialog.useDialogControl() 283 290 291 + const fullsizeFormat = useFullsizeFormat() 292 + const setFullsizeFormat = useSetFullsizeFormat() 293 + 294 + const thumbnailFormat = useThumbnailFormat() 295 + const setThumbnailFormat = useSetThumbnailFormat() 296 + 297 + const loadAsPngs = useLoadAsPngs() 298 + const setLoadAsPngs = useSetLoadAsPngs() 299 + 284 300 const deerVerificationEnabled = useDeerVerificationEnabled() 285 301 const setDeerVerificationEnabled = useSetDeerVerificationEnabled() 286 302 ··· 514 530 </Toggle.Item> 515 531 516 532 <Toggle.Item 517 - name="high_quality_images" 518 - label={_(msg`Display images in higher quality`)} 519 - value={highQualityImages} 520 - onChange={value => setHighQualityImages(value)} 521 - style={[a.w_full]}> 522 - <Toggle.LabelText style={[a.flex_1]}> 523 - <Trans>Display images in higher quality</Trans> 524 - </Toggle.LabelText> 525 - <Toggle.Platform /> 526 - </Toggle.Item> 527 - <Admonition type="info" style={[a.flex_1]}> 528 - <Trans> 529 - Images will be served as PNG instead of JPEG. Images will take 530 - longer to load and use more bandwidth. 531 - </Trans> 532 - </Admonition> 533 - 534 - <Toggle.Item 535 533 name="hide_feeds_promo_tab" 536 534 label={_(msg`Hide "Feeds ✨" tab when only one feed is selected`)} 537 535 value={hideFeedsPromoTab} ··· 571 569 style={[a.w_full]}> 572 570 <Toggle.LabelText style={[a.flex_1]}> 573 571 <Trans>Hide similar accounts recommendations</Trans> 572 + </Toggle.LabelText> 573 + <Toggle.Platform /> 574 + </Toggle.Item> 575 + </SettingsList.Group> 576 + 577 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 578 + <SettingsList.ItemIcon icon={PaintRollerIcon} /> 579 + <SettingsList.ItemText> 580 + <Trans>Images</Trans> 581 + </SettingsList.ItemText> 582 + 583 + <View style={[a.gap_md, a.w_full]}> 584 + <Text style={[a.flex_1, a.font_bold]}> 585 + <Trans>Thumbnail format</Trans> 586 + </Text> 587 + <Select.Root 588 + value={thumbnailFormat} 589 + onValueChange={value => setThumbnailFormat(value)}> 590 + <Select.Trigger label={_(msg`Select thumbnail format`)}> 591 + <Select.ValueText /> 592 + <Select.Icon /> 593 + </Select.Trigger> 594 + <Select.Content 595 + renderItem={({label, value}) => ( 596 + <Select.Item value={value} label={label}> 597 + <Select.ItemIndicator /> 598 + <Select.ItemText>{label}</Select.ItemText> 599 + </Select.Item> 600 + )} 601 + items={IMAGE_FORMATS} 602 + /> 603 + </Select.Root> 604 + </View> 605 + 606 + <View style={[a.gap_md, a.w_full]}> 607 + <Text style={[a.flex_1, a.font_bold]}> 608 + <Trans>Fullsize format</Trans> 609 + </Text> 610 + <Select.Root 611 + value={fullsizeFormat} 612 + onValueChange={value => setFullsizeFormat(value)}> 613 + <Select.Trigger label={_(msg`Select fullsize format`)}> 614 + <Select.ValueText /> 615 + <Select.Icon /> 616 + </Select.Trigger> 617 + <Select.Content 618 + renderItem={({label, value}) => ( 619 + <Select.Item value={value} label={label}> 620 + <Select.ItemIndicator /> 621 + <Select.ItemText>{label}</Select.ItemText> 622 + </Select.Item> 623 + )} 624 + items={IMAGE_FORMATS} 625 + /> 626 + </Select.Root> 627 + </View> 628 + 629 + <Toggle.Item 630 + name="load_as_pngs" 631 + label={_(msg`Load both small and 100% quality images as Pngs instead of the 632 + set image format`)} 633 + value={loadAsPngs} 634 + onChange={value => setLoadAsPngs(value)} 635 + style={[a.w_full]}> 636 + <Toggle.LabelText style={[a.flex_1]}> 637 + <Trans>Load both small and 100% quality images as Pngs</Trans> 574 638 </Toggle.LabelText> 575 639 <Toggle.Platform /> 576 640 </Toggle.Item>
+6 -2
src/state/persisted/schema.ts
··· 147 147 trusted: z.array(z.string()), 148 148 }) 149 149 .optional(), 150 - highQualityImages: z.boolean().optional(), 150 + fullsizeFormat: z.string(), 151 + thumbnailFormat: z.string(), 152 + loadAsPngs: z.boolean(), 151 153 152 154 /** @deprecated */ 153 155 mutedThreads: z.array(z.string()), ··· 239 241 'did:plc:ofbkqcjzvm6gtwuufsubnkaf', 240 242 ], 241 243 }, 242 - highQualityImages: false, 244 + thumbnailFormat: 'jpeg', 245 + fullsizeFormat: 'jpeg', 246 + loadAsPngs: true, 243 247 } 244 248 245 249 export function tryParse(rawData: string): Schema | undefined {
+47
src/state/preferences/fullsize-format.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['fullsizeFormat'] 6 + type SetContext = (v: persisted.Schema['fullsizeFormat']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.fullsizeFormat, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['fullsizeFormat']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState(persisted.get('fullsizeFormat')) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (fullsizeFormat: persisted.Schema['fullsizeFormat']) => { 20 + setState(fullsizeFormat) 21 + persisted.write('fullsizeFormat', fullsizeFormat) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate('fullsizeFormat', nextFullsizeFormat => { 28 + setState(nextFullsizeFormat) 29 + }) 30 + }, [setStateWrapped]) 31 + 32 + return ( 33 + <stateContext.Provider value={state}> 34 + <setContext.Provider value={setStateWrapped}> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + ) 39 + } 40 + 41 + export function useFullsizeFormat() { 42 + return React.useContext(stateContext) 43 + } 44 + 45 + export function useSetFullsizeFormat() { 46 + return React.useContext(setContext) 47 + }
-71
src/state/preferences/high-quality-images.tsx
··· 1 - import React from 'react' 2 - 3 - import * as persisted from '#/state/persisted' 4 - 5 - type StateContext = persisted.Schema['highQualityImages'] 6 - type SetContext = (v: persisted.Schema['highQualityImages']) => void 7 - 8 - const stateContext = React.createContext<StateContext>( 9 - persisted.defaults.highQualityImages, 10 - ) 11 - const setContext = React.createContext<SetContext>( 12 - (_: persisted.Schema['highQualityImages']) => {}, 13 - ) 14 - 15 - export function Provider({children}: React.PropsWithChildren<{}>) { 16 - const [state, setState] = React.useState(persisted.get('highQualityImages')) 17 - 18 - const setStateWrapped = React.useCallback( 19 - (highQualityImages: persisted.Schema['highQualityImages']) => { 20 - setState(highQualityImages) 21 - persisted.write('highQualityImages', highQualityImages) 22 - }, 23 - [setState], 24 - ) 25 - 26 - React.useEffect(() => { 27 - return persisted.onUpdate('highQualityImages', nextHighQualityImages => { 28 - setState(nextHighQualityImages) 29 - }) 30 - }, [setStateWrapped]) 31 - 32 - return ( 33 - <stateContext.Provider value={state}> 34 - <setContext.Provider value={setStateWrapped}> 35 - {children} 36 - </setContext.Provider> 37 - </stateContext.Provider> 38 - ) 39 - } 40 - 41 - export function useHighQualityImages() { 42 - return React.useContext(stateContext) 43 - } 44 - 45 - export function useSetHighQualityImages() { 46 - return React.useContext(setContext) 47 - } 48 - 49 - // This is a little weird to have here imo but it works I guess 50 - function modifyHighQualityImage(src: string) { 51 - try { 52 - const url = new URL(src) 53 - if (url.hostname === 'cdn.bsky.app' && url.pathname.endsWith('@jpeg')) { 54 - url.pathname = url.pathname.replace(/@jpeg$/, '@png') 55 - return url.toString() 56 - } 57 - } catch { 58 - // ignored, in case the URL is somehow malformed 59 - } 60 - 61 - return null 62 - } 63 - 64 - // Like `hackModifyThumbnailPath`, it's easier to just pipe the src into a function like this 65 - export function maybeModifyHighQualityImage(src: string, isEnabled?: boolean) { 66 - if (isEnabled) { 67 - return modifyHighQualityImage(src) ?? src 68 - } else { 69 - return src 70 - } 71 - }
+31 -28
src/state/preferences/index.tsx
··· 14 14 import {Provider as DisableSavesMetricsProvider} from './disable-saves-metrics' 15 15 import {Provider as DisableViaRepostNotificationProvider} from './disable-via-repost-notification' 16 16 import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs' 17 + import {Provider as FullsizeFormatProvider} from './fullsize-format' 17 18 import {Provider as GoLinksProvider} from './go-links-enabled' 18 19 import {Provider as HiddenPostsProvider} from './hidden-posts' 19 20 import {Provider as HideFeedsPromoTabProvider} from './hide-feeds-promo-tab' 20 21 import {Provider as HideSimilarAccountsRecommProvider} from './hide-similar-accounts-recommendations' 21 - import {Provider as HighQualityImagesProvider} from './high-quality-images' 22 22 import {Provider as InAppBrowserProvider} from './in-app-browser' 23 23 import {Provider as KawaiiProvider} from './kawaii' 24 24 import {Provider as LanguagesProvider} from './languages' ··· 28 28 import {Provider as RepostCarouselProvider} from './repost-carousel-enabled' 29 29 import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle' 30 30 import {Provider as SubtitlesProvider} from './subtitles' 31 + import {Provider as ThumbnailFormatProvider} from './thumbnail-format' 31 32 import {Provider as TrendingSettingsProvider} from './trending' 32 33 import {Provider as UsedStarterPacksProvider} from './used-starter-packs' 33 34 ··· 65 66 <ShowLinkInHandleProvider> 66 67 <LargeAltBadgeProvider> 67 68 <ExternalEmbedsProvider> 68 - <HiddenPostsProvider> 69 - <HighQualityImagesProvider> 69 + <FullsizeFormatProvider> 70 + <HiddenPostsProvider> 70 71 <InAppBrowserProvider> 71 72 <DisableHapticsProvider> 72 73 <AutoplayProvider> 73 74 <UsedStarterPacksProvider> 74 75 <SubtitlesProvider> 75 - <TrendingSettingsProvider> 76 - <RepostCarouselProvider> 77 - <KawaiiProvider> 78 - <HideFeedsPromoTabProvider> 79 - <DisableViaRepostNotificationProvider> 80 - <DisableLikesMetricsProvider> 81 - <DisableRepostsMetricsProvider> 82 - <DisableQuotesMetricsProvider> 83 - <DisableSavesMetricsProvider> 84 - <DisableReplyMetricsProvider> 85 - <HideSimilarAccountsRecommProvider> 86 - {children} 87 - </HideSimilarAccountsRecommProvider> 88 - </DisableReplyMetricsProvider> 89 - </DisableSavesMetricsProvider> 90 - </DisableQuotesMetricsProvider> 91 - </DisableRepostsMetricsProvider> 92 - </DisableLikesMetricsProvider> 93 - </DisableViaRepostNotificationProvider> 94 - </HideFeedsPromoTabProvider> 95 - </KawaiiProvider> 96 - </RepostCarouselProvider> 97 - </TrendingSettingsProvider> 76 + <ThumbnailFormatProvider> 77 + <TrendingSettingsProvider> 78 + <RepostCarouselProvider> 79 + <KawaiiProvider> 80 + <HideFeedsPromoTabProvider> 81 + <DisableViaRepostNotificationProvider> 82 + <DisableLikesMetricsProvider> 83 + <DisableRepostsMetricsProvider> 84 + <DisableQuotesMetricsProvider> 85 + <DisableSavesMetricsProvider> 86 + <DisableReplyMetricsProvider> 87 + <HideSimilarAccountsRecommProvider> 88 + {children} 89 + </HideSimilarAccountsRecommProvider> 90 + </DisableReplyMetricsProvider> 91 + </DisableSavesMetricsProvider> 92 + </DisableQuotesMetricsProvider> 93 + </DisableRepostsMetricsProvider> 94 + </DisableLikesMetricsProvider> 95 + </DisableViaRepostNotificationProvider> 96 + </HideFeedsPromoTabProvider> 97 + </KawaiiProvider> 98 + </RepostCarouselProvider> 99 + </TrendingSettingsProvider> 100 + </ThumbnailFormatProvider> 98 101 </SubtitlesProvider> 99 102 </UsedStarterPacksProvider> 100 103 </AutoplayProvider> 101 104 </DisableHapticsProvider> 102 105 </InAppBrowserProvider> 103 - </HighQualityImagesProvider> 104 - </HiddenPostsProvider> 106 + </HiddenPostsProvider> 107 + </FullsizeFormatProvider> 105 108 </ExternalEmbedsProvider> 106 109 </LargeAltBadgeProvider> 107 110 </ShowLinkInHandleProvider>
+47
src/state/preferences/load-small-pngs.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['loadAsPngs'] 6 + type SetContext = (v: persisted.Schema['loadAsPngs']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.loadAsPngs, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['loadAsPngs']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState(persisted.get('loadAsPngs')) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (loadAsPngs: persisted.Schema['loadAsPngs']) => { 20 + setState(loadAsPngs) 21 + persisted.write('loadAsPngs', loadAsPngs) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate('loadAsPngs', nextLoadAsPngs => { 28 + setState(nextLoadAsPngs) 29 + }) 30 + }, [setStateWrapped]) 31 + 32 + return ( 33 + <stateContext.Provider value={state}> 34 + <setContext.Provider value={setStateWrapped}> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + ) 39 + } 40 + 41 + export function useLoadAsPngs() { 42 + return React.useContext(stateContext) 43 + } 44 + 45 + export function useSetLoadAsPngs() { 46 + return React.useContext(setContext) 47 + }
+47
src/state/preferences/thumbnail-format.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['thumbnailFormat'] 6 + type SetContext = (v: persisted.Schema['thumbnailFormat']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.thumbnailFormat, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['thumbnailFormat']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState(persisted.get('thumbnailFormat')) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (thumbnailFormat: persisted.Schema['thumbnailFormat']) => { 20 + setState(thumbnailFormat) 21 + persisted.write('thumbnailFormat', thumbnailFormat) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate('thumbnailFormat', nextThumbnailFormat => { 28 + setState(nextThumbnailFormat) 29 + }) 30 + }, [setStateWrapped]) 31 + 32 + return ( 33 + <stateContext.Provider value={state}> 34 + <setContext.Provider value={setStateWrapped}> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + ) 39 + } 40 + 41 + export function useThumbnailFormat() { 42 + return React.useContext(stateContext) 43 + } 44 + 45 + export function useSetThumbnailFormat() { 46 + return React.useContext(setContext) 47 + }
+5
src/types/bsky/post.ts
··· 160 160 quality?: number 161 161 size?: number 162 162 } 163 + 164 + export interface BetterImages { 165 + $type?: 'app.bsky.embed.images' 166 + images: BetterImage[] 167 + }
+1
src/view/com/post/Post.tsx
··· 219 219 {post.embed ? ( 220 220 <Embed 221 221 embed={post.embed} 222 + recordEmbed={record.embed} 222 223 moderation={moderation} 223 224 viewContext={PostEmbedViewContext.Feed} 224 225 />
+4
src/view/com/posts/PostFeedItem.tsx
··· 460 460 moderation={moderation} 461 461 richText={richText} 462 462 postEmbed={post.embed} 463 + recordEmbed={record.embed} 463 464 postAuthor={post.author} 464 465 onOpenEmbed={onOpenEmbed} 465 466 post={post} ··· 491 492 moderation, 492 493 richText, 493 494 postEmbed, 495 + recordEmbed, 494 496 postAuthor, 495 497 onOpenEmbed, 496 498 threadgateRecord, ··· 498 500 moderation: ModerationDecision 499 501 richText: RichTextAPI 500 502 postEmbed: AppBskyFeedDefs.PostView['embed'] 503 + recordEmbed: AppBskyFeedPost.Record['embed'] 501 504 postAuthor: AppBskyFeedDefs.PostView['author'] 502 505 onOpenEmbed: () => void 503 506 post: AppBskyFeedDefs.PostView ··· 566 569 <View style={[a.pb_xs]}> 567 570 <Embed 568 571 embed={postEmbed} 572 + recordEmbed={recordEmbed} 569 573 moderation={moderation} 570 574 onOpen={onOpenEmbed} 571 575 viewContext={PostEmbedViewContext.Feed}
+9 -11
src/view/com/util/UserAvatar.tsx
··· 24 24 import {compressIfNeeded} from '#/lib/media/manip' 25 25 import {openCamera, openCropper, openPicker} from '#/lib/media/picker' 26 26 import {type PickerImage} from '#/lib/media/picker.shared' 27 + import {modifyImageFormat} from '#/lib/media/util' 27 28 import {makeProfileLink} from '#/lib/routes/links' 28 29 import {sanitizeDisplayName} from '#/lib/strings/display-names' 29 30 import {sanitizeHandle} from '#/lib/strings/handles' ··· 34 35 compressImage, 35 36 createComposerImage, 36 37 } from '#/state/gallery' 37 - import { 38 - maybeModifyHighQualityImage, 39 - useHighQualityImages, 40 - } from '#/state/preferences/high-quality-images' 38 + import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' 41 39 import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' 42 40 import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog' 43 41 import {HighPriorityImage} from '#/view/com/util/images/Image' ··· 228 226 }: UserAvatarProps): React.ReactNode => { 229 227 const t = useTheme() 230 228 const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square') 231 - const highQualityImages = useHighQualityImages() 229 + const thumbnailFormat = useThumbnailFormat() 232 230 233 231 const aviStyle = useMemo(() => { 234 232 let borderRadius ··· 299 297 style={aviStyle} 300 298 resizeMode="cover" 301 299 source={{ 302 - uri: maybeModifyHighQualityImage( 300 + uri: modifyImageFormat( 303 301 hackModifyThumbnailPath(avatar, size < 90), 304 - highQualityImages, 302 + thumbnailFormat, 305 303 ), 306 304 }} 307 305 blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} ··· 313 311 style={aviStyle} 314 312 contentFit="cover" 315 313 source={{ 316 - uri: maybeModifyHighQualityImage( 314 + uri: modifyImageFormat( 317 315 hackModifyThumbnailPath(avatar, size < 90), 318 - highQualityImages, 316 + thumbnailFormat, 319 317 ), 320 318 }} 321 319 blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} ··· 356 354 const editImageDialogControl = useDialogControl() 357 355 358 356 const sheetWrapper = useSheetWrapper() 359 - const highQualityImages = useHighQualityImages() 357 + const thumbnailFormat = useThumbnailFormat() 360 358 361 359 const circular = type !== 'algo' && type !== 'list' 362 360 ··· 456 454 testID="userAvatarImage" 457 455 style={aviStyle} 458 456 source={{ 459 - uri: maybeModifyHighQualityImage(avatar, highQualityImages), 457 + uri: modifyImageFormat(avatar, thumbnailFormat), 460 458 }} 461 459 accessibilityRole="image" 462 460 />
+10 -13
src/view/com/util/UserBanner.tsx
··· 24 24 import {compressIfNeeded} from '#/lib/media/manip' 25 25 import {openCamera, openCropper, openPicker} from '#/lib/media/picker' 26 26 import {type PickerImage} from '#/lib/media/picker.shared' 27 + import {modifyImageFormat} from '#/lib/media/util' 27 28 import {logger} from '#/logger' 28 29 import {isAndroid, isNative} from '#/platform/detection' 29 30 import { ··· 32 33 createComposerImage, 33 34 } from '#/state/gallery' 34 35 import {useLightboxControls} from '#/state/lightbox' 35 - import { 36 - maybeModifyHighQualityImage, 37 - useHighQualityImages, 38 - } from '#/state/preferences/high-quality-images' 36 + import {useFullsizeFormat} from '#/state/preferences/fullsize-format' 37 + import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' 39 38 import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog' 40 39 import {EventStopper} from '#/view/com/util/EventStopper' 41 40 import {atoms as a, tokens, useTheme} from '#/alf' ··· 68 67 const [rawImage, setRawImage] = useState<ComposerImage | undefined>() 69 68 const editImageDialogControl = useDialogControl() 70 69 const {openLightbox} = useLightboxControls() 71 - const highQualityImages = useHighQualityImages() 70 + const fullsizeFormat = useFullsizeFormat() 71 + const thumbnailFormat = useThumbnailFormat() 72 72 73 73 const bannerRef = useAnimatedRef() 74 74 ··· 129 129 openLightbox({ 130 130 images: [ 131 131 { 132 - uri: maybeModifyHighQualityImage(uri, highQualityImages), 133 - thumbUri: maybeModifyHighQualityImage(uri, highQualityImages), 132 + uri: modifyImageFormat(uri, fullsizeFormat), 133 + thumbUri: modifyImageFormat(uri, thumbnailFormat), 134 134 thumbRect, 135 135 dimensions: thumbRect, 136 136 thumbDimensions: null, ··· 140 140 index: 0, 141 141 }) 142 142 }, 143 - [openLightbox, highQualityImages], 143 + [openLightbox, fullsizeFormat, thumbnailFormat], 144 144 ) 145 145 146 146 const onPressBanner = useCallback(() => { ··· 174 174 testID="userBannerImage" 175 175 style={styles.bannerImage} 176 176 source={{ 177 - uri: maybeModifyHighQualityImage( 178 - banner, 179 - highQualityImages, 180 - ), 177 + uri: modifyImageFormat(banner, thumbnailFormat), 181 178 }} 182 179 accessible={true} 183 180 accessibilityIgnoresInvertColors ··· 271 268 testID="userBannerImage" 272 269 style={[styles.bannerImage, t.atoms.bg_contrast_25]} 273 270 contentFit="cover" 274 - source={{uri: maybeModifyHighQualityImage(banner, highQualityImages)}} 271 + source={{uri: modifyImageFormat(banner, fullsizeFormat)}} 275 272 blurRadius={moderation?.blur ? 100 : 0} 276 273 accessible={true} 277 274 accessibilityIgnoresInvertColors
+1 -6
src/view/com/util/images/AutoSizedImage.tsx
··· 11 11 12 12 import {type Dimensions} from '#/lib/media/types' 13 13 import {isNative} from '#/platform/detection' 14 - import { 15 - maybeModifyHighQualityImage, 16 - useHighQualityImages, 17 - } from '#/state/preferences/high-quality-images' 18 14 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 19 15 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 20 16 import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' ··· 89 85 const largeAlt = useLargeAltBadgeEnabled() 90 86 const containerRef = useAnimatedRef() 91 87 const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) 92 - const highQualityImages = useHighQualityImages() 93 88 94 89 let aspectRatio: number | undefined 95 90 const dims = image.aspectRatio ··· 120 115 <Image 121 116 contentFit={isContain ? 'contain' : 'cover'} 122 117 style={[a.w_full, a.h_full]} 123 - source={maybeModifyHighQualityImage(image.thumb, highQualityImages)} 118 + source={image.thumb} 124 119 accessible={true} // Must set for `accessibilityLabel` to work 125 120 accessibilityIgnoresInvertColors 126 121 accessibilityLabel={image.alt}
+4 -6
src/view/com/util/images/Gallery.tsx
··· 6 6 import {useLingui} from '@lingui/react' 7 7 8 8 import {type Dimensions} from '#/lib/media/types' 9 - import { 10 - maybeModifyHighQualityImage, 11 - useHighQualityImages, 12 - } from '#/state/preferences/high-quality-images' 9 + import {modifyImageFormat} from '#/lib/media/util' 13 10 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 11 + import {useThumbnailFormat} from '#/state/preferences/thumbnail-format' 14 12 import {atoms as a, useTheme} from '#/alf' 15 13 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 16 14 import {PostEmbedViewContext} from '#/components/Post/Embed/types' ··· 50 48 const t = useTheme() 51 49 const {_} = useLingui() 52 50 const largeAltBadge = useLargeAltBadgeEnabled() 53 - const highQualityImages = useHighQualityImages() 51 + const thumbnailFormat = useThumbnailFormat() 54 52 const image = images[index] 55 53 const hasAlt = !!image.alt 56 54 const hideBadges = ··· 76 74 accessibilityHint=""> 77 75 <Image 78 76 source={{ 79 - uri: maybeModifyHighQualityImage(image.thumb, highQualityImages), 77 + uri: modifyImageFormat(image.thumb, thumbnailFormat), 80 78 }} 81 79 style={[a.flex_1]} 82 80 accessible={true}