···11import React from 'react'
22import {msg} from '@lingui/core/macro'
33-import {Plural, Trans} from '@lingui/react/macro'
43import {useLingui} from '@lingui/react'
44+import {Plural, Trans} from '@lingui/react/macro'
55import {useFocusEffect} from '@react-navigation/native'
6677import {useSetTitle} from '#/lib/hooks/useSetTitle'
+1-1
src/screens/Post/PostQuotes.tsx
···11import React from 'react'
22import {msg} from '@lingui/core/macro'
33-import {Plural, Trans} from '@lingui/react/macro'
43import {useLingui} from '@lingui/react'
44+import {Plural, Trans} from '@lingui/react/macro'
55import {useFocusEffect} from '@react-navigation/native'
6677import {useSetTitle} from '#/lib/hooks/useSetTitle'
+1-1
src/screens/Post/PostRepostedBy.tsx
···11import React from 'react'
22import {msg} from '@lingui/core/macro'
33-import {Plural, Trans} from '@lingui/react/macro'
43import {useLingui} from '@lingui/react'
44+import {Plural, Trans} from '@lingui/react/macro'
55import {useFocusEffect} from '@react-navigation/native'
6677import {useSetTitle} from '#/lib/hooks/useSetTitle'
+17-9
src/screens/Profile/Header/Shell.tsx
···2020import {useLightboxControls} from '#/state/lightbox'
2121import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars'
2222import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
2323+import {useHighQualityImages} from '#/state/preferences/high-quality-images'
2324import {
2424- maybeModifyHighQualityImage,
2525- useHighQualityImages,
2626-} from '#/state/preferences/high-quality-images'
2525+ applyImageTransforms,
2626+ useImageCdnHost,
2727+} from '#/state/preferences/image-cdn-host'
2728import {useSession} from '#/state/session'
2829import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
2930import {UserAvatar} from '#/view/com/util/UserAvatar'
···6869 const playHaptic = useHaptics()
6970 const liveStatusControl = useDialogControl()
7071 const highQualityImages = useHighQualityImages()
7272+ const imageCdnHost = useImageCdnHost()
7173 const enableSquareAvatars = useEnableSquareAvatars()
7274 const enableSquareButtons = useEnableSquareButtons()
7375···102104 openLightbox({
103105 images: [
104106 {
105105- uri: maybeModifyHighQualityImage(uri, highQualityImages),
106106- thumbUri: maybeModifyHighQualityImage(uri, highQualityImages),
107107+ uri: applyImageTransforms(uri, {imageCdnHost, highQualityImages}),
108108+ thumbUri: applyImageTransforms(uri, {
109109+ imageCdnHost,
110110+ highQualityImages,
111111+ }),
107112 thumbRect,
108113 dimensions:
109114 type === 'circle-avi' || type === 'rect-avi'
···124129 index: 0,
125130 })
126131 },
127127- [openLightbox, highQualityImages, enableSquareAvatars],
132132+ [openLightbox, imageCdnHost, highQualityImages, enableSquareAvatars],
128133 )
129134130135 // theres probs a better way instead of just making a separate one but this works:tm: so its whatever
···133138 openLightbox({
134139 images: [
135140 {
136136- uri: maybeModifyHighQualityImage(uri, highQualityImages),
137137- thumbUri: maybeModifyHighQualityImage(uri, highQualityImages),
141141+ uri: applyImageTransforms(uri, {imageCdnHost, highQualityImages}),
142142+ thumbUri: applyImageTransforms(uri, {
143143+ imageCdnHost,
144144+ highQualityImages,
145145+ }),
138146 thumbRect,
139147 dimensions: thumbRect,
140148 thumbDimensions: null,
···144152 index: 0,
145153 })
146154 },
147147- [openLightbox, highQualityImages],
155155+ [openLightbox, imageCdnHost, highQualityImages],
148156 )
149157150158 const isMe = useMemo(
+1-1
src/screens/Profile/ProfileFollowers.tsx
···11import React from 'react'
22import {msg} from '@lingui/core/macro'
33-import {Plural} from '@lingui/react/macro'
43import {useLingui} from '@lingui/react'
44+import {Plural} from '@lingui/react/macro'
55import {useFocusEffect} from '@react-navigation/native'
6677import {useSetTitle} from '#/lib/hooks/useSetTitle'
+1-1
src/screens/Profile/ProfileFollows.tsx
···11import React from 'react'
22import {msg} from '@lingui/core/macro'
33-import {Plural} from '@lingui/react/macro'
43import {useLingui} from '@lingui/react'
44+import {Plural} from '@lingui/react/macro'
55import {useFocusEffect} from '@react-navigation/native'
6677import {useSetTitle} from '#/lib/hooks/useSetTitle'
+109-4
src/screens/Settings/RunesSettings.tsx
···11import {useState} from 'react'
22import {View} from 'react-native'
33import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
44-import {msg, Trans} from '@lingui/macro'
44+import {msg} from '@lingui/core/macro'
55import {useLingui} from '@lingui/react'
66+import {Trans} from '@lingui/react/macro'
67import {type NativeStackScreenProps} from '@react-navigation/native-stack'
7889import {DEFAULT_ALT_TEXT_AI_MODEL} from '#/lib/constants'
···9899 useHighQualityImages,
99100 useSetHighQualityImages,
100101} from '#/state/preferences/high-quality-images'
102102+import {
103103+ useImageCdnHost,
104104+ useSetImageCdnHost,
105105+} from '#/state/preferences/image-cdn-host'
101106import {useModerationOpts} from '#/state/preferences/moderation-opts'
102107import {
103108 useNoAppLabelers,
···319324 )
320325}
321326327327+function ImageCdnHostDialog({control}: {control: Dialog.DialogControlProps}) {
328328+ const pal = usePalette('default')
329329+ const {_} = useLingui()
330330+331331+ const imageCdnHost = useImageCdnHost()
332332+ const [url, setUrl] = useState(imageCdnHost ?? '')
333333+ const setImageCdnHost = useSetImageCdnHost()
334334+335335+ const submit = () => {
336336+ try {
337337+ setImageCdnHost(new URL(url).origin)
338338+ } catch {
339339+ setImageCdnHost(url)
340340+ }
341341+ control.close()
342342+ }
343343+344344+ const shouldDisable = () => {
345345+ try {
346346+ return !new URL(url).hostname.includes('.')
347347+ } catch (e) {
348348+ return true
349349+ }
350350+ }
351351+352352+ return (
353353+ <Dialog.Outer
354354+ control={control}
355355+ nativeOptions={{preventExpansion: true}}
356356+ onClose={() => setUrl(imageCdnHost ?? '')}>
357357+ <Dialog.Handle />
358358+ <Dialog.ScrollableInner label={_(msg`Image CDN URL`)}>
359359+ <View style={[a.gap_sm, a.pb_lg]}>
360360+ <Text style={[a.text_2xl, a.font_bold]}>
361361+ <Trans>Image CDN URL</Trans>
362362+ </Text>
363363+ </View>
364364+365365+ <View style={a.gap_lg}>
366366+ <Dialog.Input
367367+ label="Text input field"
368368+ autoFocus
369369+ style={[styles.textInput, pal.border, pal.text]}
370370+ onChangeText={value => {
371371+ setUrl(value)
372372+ }}
373373+ placeholder={persisted.defaults.imageCdnHost}
374374+ placeholderTextColor={pal.colors.textLight}
375375+ onSubmitEditing={submit}
376376+ accessibilityHint={_(msg`Input the URL of the image CDN to use`)}
377377+ defaultValue={imageCdnHost}
378378+ />
379379+380380+ <View style={IS_WEB && [a.flex_row, a.justify_end]}>
381381+ <Button
382382+ label={_(msg`Save`)}
383383+ size="large"
384384+ onPress={submit}
385385+ variant="solid"
386386+ color="primary"
387387+ disabled={shouldDisable()}>
388388+ <ButtonText>
389389+ <Trans>Save</Trans>
390390+ </ButtonText>
391391+ </Button>
392392+ </View>
393393+ </View>
394394+395395+ <Dialog.Close />
396396+ </Dialog.ScrollableInner>
397397+ </Dialog.Outer>
398398+ )
399399+}
400400+322401function PostReplacementDialog({
323402 control,
324403}: {
···657736658737 const highQualityImages = useHighQualityImages()
659738 const setHighQualityImages = useSetHighQualityImages()
739739+ const imageCdnHost = useImageCdnHost()
660740661741 const hideFeedsPromoTab = useHideFeedsPromoTab()
662742 const setHideFeedsPromoTab = useSetHideFeedsPromoTab()
···733813734814 const setLibreTranslateInstanceControl = Dialog.useDialogControl()
735815816816+ const setImageCdnHostControl = Dialog.useDialogControl()
817817+736818 const setPostReplacementDialogControl = Dialog.useDialogControl()
737819738820 const setOpenRouterApiKeyControl = Dialog.useDialogControl()
···10071089 longer to load and use more bandwidth.
10081090 </Trans>
10091091 </Admonition>
10101010-10111092 <Toggle.Item
10121093 name="hide_feeds_promo_tab"
10131094 label={_(msg`Hide "Feeds ✨" tab when only one feed is selected`)}
···10961177 <Admonition type="warning" style={[a.flex_1]}>
10971178 <Trans>
10981179 This only gets rid of the reminder on app launch, useful if your
10991099- PDS does not have email verification setup.\nThis does NOT give
11001100- access to features locked behind email verification.
11801180+ PDS does not have email verification setup.\u00A0 This does NOT
11811181+ give access to features locked behind email verification.
11011182 </Trans>
11021183 </Admonition>
11031184···1358143913591440 <SettingsList.Divider />
1360144114421442+ <SettingsList.Item>
14431443+ <SettingsList.ItemIcon icon={EarthIcon} />
14441444+ <SettingsList.ItemText>
14451445+ <Trans>{`Image CDN`}</Trans>
14461446+ </SettingsList.ItemText>
14471447+ <SettingsList.BadgeButton
14481448+ label={_(msg`Change`)}
14491449+ onPress={() => setImageCdnHostControl.open()}
14501450+ />
14511451+ </SettingsList.Item>
14521452+ <SettingsList.Item>
14531453+ <Admonition type="info" style={[a.flex_1]}>
14541454+ <Trans>
14551455+ Override the CDN host for all images. Current:
14561456+ <InlineLinkText to={imageCdnHost} label={imageCdnHost}>
14571457+ {imageCdnHost}
14581458+ </InlineLinkText>
14591459+ </Trans>
14601460+ </Admonition>
14611461+ </SettingsList.Item>
14621462+14631463+ <SettingsList.Divider />
14641464+13611465 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
13621466 <SettingsList.ItemIcon icon={RaisingHandIcon} />
13631467 <SettingsList.ItemText>
···14061510 <LibreTranslateInstanceDialog
14071511 control={setLibreTranslateInstanceControl}
14081512 />
15131513+ <ImageCdnHostDialog control={setImageCdnHostControl} />
14091514 <PostReplacementDialog control={setPostReplacementDialogControl} />
14101515 <OpenRouterApiKeyDialog control={setOpenRouterApiKeyControl} />
14111516 <OpenRouterModelDialog control={setOpenRouterModelControl} />
···5656 UserCircle_Stroke2_Corner0_Rounded as UserCircle,
5757} from '#/components/icons/UserCircle'
5858import {InlineLinkText} from '#/components/Link'
5959-import {Text} from '#/components/Typography'
6059import {PdsBadge} from '#/components/PdsBadge'
6060+import {Text} from '#/components/Typography'
6161import {useSimpleVerificationState} from '#/components/verification'
6262import {VerificationCheck} from '#/components/verification/VerificationCheck'
6363import {IS_WEB} from '#/env'