Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

remove analytics, improve direct fetch ui and perf

commit cd5ba34096d7d0d83b279aaaed725c7991bc8c60
Author: Aviva Ruben <aviva@rubenfamily.com>
Date: Wed Apr 9 11:30:25 2025 -0500

improve direct fetch peformance and ui

commit 54bc155b4da8ad120abb0864a2957cd76efbf571
Author: Aviva Ruben <aviva@rubenfamily.com>
Date: Wed Apr 9 11:03:02 2025 -0500

configurable geolocation

commit 0ce84d99ab9a066490e2e5a44c6bd5b987fa591a
Author: Aviva Ruben <aviva@rubenfamily.com>
Date: Wed Apr 9 00:08:01 2025 -0500

don't use sentry transport

+201 -95
+111 -1
src/screens/Settings/DeerSettings.tsx
··· 1 1 import {useState} from 'react' 2 + import {View} from 'react-native' 2 3 import {msg, Trans} from '@lingui/macro' 3 4 import {useLingui} from '@lingui/react' 4 5 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 5 6 7 + import {usePalette} from '#/lib/hooks/usePalette' 6 8 import {type CommonNavigatorParams} from '#/lib/routes/types' 7 9 import {type Gate} from '#/lib/statsig/gates' 8 10 import { ··· 10 12 useDangerousSetGate, 11 13 useGatesCache, 12 14 } from '#/lib/statsig/statsig' 15 + import {isWeb} from '#/platform/detection' 16 + import {setGeolocation, useGeolocation} from '#/state/geolocation' 13 17 import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences' 14 18 import { 15 19 useConstellationEnabled, ··· 19 23 useDirectFetchRecords, 20 24 useSetDirectFetchRecords, 21 25 } from '#/state/preferences/direct-fetch-records' 26 + import {TextInput} from '#/view/com/modals/util' 22 27 import * as SettingsList from '#/screens/Settings/components/SettingsList' 23 28 import {atoms as a} from '#/alf' 24 29 import {Admonition} from '#/components/Admonition' 30 + import {Button, ButtonText} from '#/components/Button' 31 + import * as Dialog from '#/components/Dialog' 25 32 import * as Toggle from '#/components/forms/Toggle' 26 33 import {Atom_Stroke2_Corner0_Rounded as DeerIcon} from '#/components/icons/Atom' 27 34 import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' 35 + import {Earth_Stroke2_Corner2_Rounded as GlobeIcon} from '#/components/icons/Globe' 36 + import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' 28 37 import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 29 38 import * as Layout from '#/components/Layout' 39 + import {Text} from '#/components/Typography' 30 40 31 41 type Props = NativeStackScreenProps<CommonNavigatorParams> 32 42 43 + function GeolocationSettingsDialog({ 44 + control, 45 + }: { 46 + control: Dialog.DialogControlProps 47 + }) { 48 + const pal = usePalette('default') 49 + const {_} = useLingui() 50 + 51 + const [hasChanged, setHasChanged] = useState(false) 52 + const [countryCode, setCountryCode] = useState('') 53 + 54 + const submit = () => { 55 + setGeolocation({countryCode}) 56 + control.close() 57 + } 58 + 59 + return ( 60 + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 61 + <Dialog.Handle /> 62 + <Dialog.ScrollableInner label={_(msg`Geolocation ISO 3166-1 Code`)}> 63 + <View style={[a.gap_sm, a.pb_lg]}> 64 + <Text style={[a.text_2xl, a.font_bold]}> 65 + <Trans>Geolocation ISO 3166-1 Code</Trans> 66 + </Text> 67 + </View> 68 + 69 + <View style={a.gap_lg}> 70 + <TextInput 71 + accessibilityLabel="Text input field" 72 + autoFocus 73 + style={[styles.textInput, pal.border, pal.text]} 74 + value={countryCode} 75 + onChangeText={value => { 76 + setCountryCode(value.toUpperCase()) 77 + setHasChanged(true) 78 + }} 79 + maxLength={2} 80 + placeholder="BR" 81 + placeholderTextColor={pal.colors.textLight} 82 + onSubmitEditing={submit} 83 + accessibilityHint={_( 84 + msg`Input 2 letter ISO 3166-1 country code to use as location`, 85 + )} 86 + /> 87 + 88 + <View style={isWeb && [a.flex_row, a.justify_end]}> 89 + <Button 90 + label={hasChanged ? _(msg`Save location`) : _(msg`Done`)} 91 + size="large" 92 + onPress={submit} 93 + variant="solid" 94 + color="primary"> 95 + <ButtonText> 96 + {hasChanged ? <Trans>Save</Trans> : <Trans>Done</Trans>} 97 + </ButtonText> 98 + </Button> 99 + </View> 100 + </View> 101 + 102 + <Dialog.Close /> 103 + </Dialog.ScrollableInner> 104 + </Dialog.Outer> 105 + ) 106 + } 107 + 33 108 export function DeerSettingsScreen({}: Props) { 34 109 const {_} = useLingui() 35 110 ··· 41 116 42 117 const directFetchRecords = useDirectFetchRecords() 43 118 const setDirectFetchRecords = useSetDirectFetchRecords() 119 + 120 + const location = useGeolocation() 121 + const setLocationControl = Dialog.useDialogControl() 44 122 45 123 const [gates, setGatesView] = useState(Object.fromEntries(useGatesCache())) 46 124 const dangerousSetGate = useDangerousSetGate() ··· 121 199 </Toggle.Item> 122 200 </SettingsList.Group> 123 201 202 + <SettingsList.Item> 203 + <SettingsList.ItemIcon icon={GlobeIcon} /> 204 + <SettingsList.ItemText> 205 + <Trans>{`ISO 3166-1 Location (currently ${ 206 + location.geolocation?.countryCode ?? '?' 207 + })`}</Trans> 208 + </SettingsList.ItemText> 209 + <SettingsList.BadgeButton 210 + label={_(msg`Change`)} 211 + onPress={() => setLocationControl.open()} 212 + /> 213 + </SettingsList.Item> 214 + <SettingsList.Item> 215 + <Admonition type="info" style={[a.flex_1]}> 216 + <Trans> 217 + Geolocation country code informs required regional labelers and 218 + currency behavior. 219 + </Trans> 220 + </Admonition> 221 + </SettingsList.Item> 222 + 124 223 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 125 224 <SettingsList.ItemIcon icon={PaintRollerIcon} /> 126 225 <SettingsList.ItemText> ··· 141 240 </SettingsList.Group> 142 241 143 242 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 144 - <SettingsList.ItemIcon icon={PaintRollerIcon} /> 243 + <SettingsList.ItemIcon icon={BeakerIcon} /> 145 244 <SettingsList.ItemText> 146 245 <Trans>Gates</Trans> 147 246 </SettingsList.ItemText> ··· 176 275 </SettingsList.Item> 177 276 </SettingsList.Container> 178 277 </Layout.Content> 278 + <GeolocationSettingsDialog control={setLocationControl} /> 179 279 </Layout.Screen> 180 280 ) 181 281 } 282 + 283 + const styles = { 284 + textInput: { 285 + borderWidth: 1, 286 + borderRadius: 6, 287 + paddingHorizontal: 14, 288 + paddingVertical: 10, 289 + fontSize: 16, 290 + }, 291 + }
+8 -64
src/state/geolocation.tsx
··· 1 1 import React from 'react' 2 2 import EventEmitter from 'eventemitter3' 3 3 4 - import {networkRetry} from '#/lib/async/retry' 5 4 import {logger} from '#/logger' 6 - import {Device, device} from '#/storage' 5 + import {type Device, device} from '#/storage' 7 6 8 7 const events = new EventEmitter() 9 8 const EVENT = 'geolocation-updated' ··· 24 23 * additional mod authorities. 25 24 */ 26 25 export const DEFAULT_GEOLOCATION: Device['geolocation'] = { 27 - countryCode: undefined, 28 - } 29 - 30 - async function getGeolocation(): Promise<Device['geolocation']> { 31 - const res = await fetch(`https://bsky.app/ipcc`) 32 - 33 - if (!res.ok) { 34 - throw new Error(`geolocation: lookup failed ${res.status}`) 35 - } 36 - 37 - const json = await res.json() 38 - 39 - if (json.countryCode) { 40 - return { 41 - countryCode: json.countryCode, 42 - } 43 - } else { 44 - return undefined 45 - } 26 + countryCode: 'US', 46 27 } 47 28 48 29 /** ··· 64 45 * In dev, IP server is unavailable, so we just set the default geolocation 65 46 * and fail closed. 66 47 */ 67 - if (__DEV__) { 68 - geolocationResolution = new Promise(y => y()) 48 + geolocationResolution = new Promise(y => y()) 49 + if (device.get(['geolocation']) == undefined) { 69 50 device.set(['geolocation'], DEFAULT_GEOLOCATION) 70 - return 71 51 } 72 - 73 - geolocationResolution = new Promise(async resolve => { 74 - try { 75 - // Try once, fail fast 76 - const geolocation = await getGeolocation() 77 - if (geolocation) { 78 - device.set(['geolocation'], geolocation) 79 - emitGeolocationUpdate(geolocation) 80 - logger.debug(`geolocation: success`, {geolocation}) 81 - } else { 82 - // endpoint should throw on all failures, this is insurance 83 - throw new Error(`geolocation: nothing returned from initial request`) 84 - } 85 - } catch (e: any) { 86 - logger.error(`geolocation: failed initial request`, { 87 - safeMessage: e.message, 88 - }) 52 + } 89 53 90 - // set to default 91 - device.set(['geolocation'], DEFAULT_GEOLOCATION) 92 - 93 - // retry 3 times, but don't await, proceed with default 94 - networkRetry(3, getGeolocation) 95 - .then(geolocation => { 96 - if (geolocation) { 97 - device.set(['geolocation'], geolocation) 98 - emitGeolocationUpdate(geolocation) 99 - logger.debug(`geolocation: success`, {geolocation}) 100 - } else { 101 - // endpoint should throw on all failures, this is insurance 102 - throw new Error(`geolocation: nothing returned from retries`) 103 - } 104 - }) 105 - .catch((e: any) => { 106 - // complete fail closed 107 - logger.error(`geolocation: failed retries`, {safeMessage: e.message}) 108 - }) 109 - } finally { 110 - resolve(undefined) 111 - } 112 - }) 54 + export function setGeolocation(geolocation: Device['geolocation']) { 55 + device.set(['geolocation'], geolocation) 56 + emitGeolocationUpdate(geolocation) 113 57 } 114 58 115 59 /**
+47
src/state/preferences/country-code.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['countryCode'] 6 + type SetContext = (v: persisted.Schema['countryCode']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.countryCode, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['countryCode']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState(persisted.get('countryCode')) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (countryCode: persisted.Schema['countryCode']) => { 20 + setState(countryCode) 21 + persisted.write('countryCode', countryCode) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate('countryCode', nextCountryCode => { 28 + setState(nextCountryCode) 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 useCountryCode() { 42 + return React.useContext(stateContext) 43 + } 44 + 45 + export function useSetCountryCode() { 46 + return React.useContext(setContext) 47 + }
+23 -22
src/state/queries/direct-fetch-record.ts
··· 32 32 } 33 33 34 34 try { 35 - // TODO: parallel, series fetch sucks there isn't a dependency 36 - const profile = (await agent.getProfile({actor: urip.host})).data 37 - const {data} = await retry( 38 - 2, 39 - e => { 40 - if (e.message.includes(`Could not locate record:`)) { 41 - return false 42 - } 43 - return true 44 - }, 45 - () => 46 - agent.api.com.atproto.repo.getRecord({ 47 - repo: urip.host, 48 - collection: 'app.bsky.feed.post', 49 - rkey: urip.rkey, 50 - }), 51 - ) 52 - if ( 53 - data.value && 54 - bsky.validate(data.value, AppBskyFeedPost.validateRecord) 55 - ) { 56 - const record = data.value 35 + const [profile, record] = await Promise.all([ 36 + (async () => (await agent.getProfile({actor: urip.host})).data)(), 37 + (async () => 38 + ( 39 + await retry( 40 + 2, 41 + e => { 42 + if (e.message.includes(`Could not locate record:`)) { 43 + return false 44 + } 45 + return true 46 + }, 47 + () => 48 + agent.api.com.atproto.repo.getRecord({ 49 + repo: urip.host, 50 + collection: 'app.bsky.feed.post', 51 + rkey: urip.rkey, 52 + }), 53 + ) 54 + ).data.value)(), 55 + ]) 56 + 57 + if (record && bsky.validate(record, AppBskyFeedPost.validateRecord)) { 57 58 return { 58 59 $type: 'app.bsky.embed.record#viewRecord', 59 60 uri,
+12 -8
src/view/com/util/post-embeds/QuoteEmbed.tsx
··· 37 37 import {useSession} from '#/state/session' 38 38 import {atoms as a, useTheme} from '#/alf' 39 39 import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash' 40 + import {Loader} from '#/components/Loader' 40 41 import {RichText} from '#/components/RichText' 41 42 import {SubtleWebHover} from '#/components/SubtleWebHover' 42 43 import * as bsky from '#/types/bsky' ··· 114 115 return ( 115 116 <View 116 117 style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> 117 - <InfoCircleIcon size={18} style={pal.text} /> 118 + {directFetchEnabled ? ( 119 + <Loader size={'md'} style={pal.text} /> 120 + ) : ( 121 + <InfoCircleIcon size={18} style={pal.text} /> 122 + )} 118 123 <Text type="lg" style={pal.text}> 119 - {directFetchEnabled ? ( 120 - <Trans>Blocked...</Trans> 121 - ) : ( 122 - <Trans>Blocked</Trans> 123 - )} 124 + <Trans>Blocked</Trans> 124 125 </Text> 125 126 </View> 126 127 ) ··· 160 161 return ( 161 162 <View 162 163 style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> 163 - <InfoCircleIcon size={18} style={pal.text} /> 164 + {directFetchEnabled ? ( 165 + <Loader size={'md'} style={pal.text} /> 166 + ) : ( 167 + <InfoCircleIcon size={18} style={pal.text} /> 168 + )} 164 169 <Text type="lg" style={pal.text}> 165 170 {isViewerOwner ? ( 166 171 <Trans>Removed by you</Trans> 167 172 ) : ( 168 173 <Trans>Removed by author</Trans> 169 174 )} 170 - {directFetchEnabled ? <Trans>...</Trans> : undefined} 171 175 </Text> 172 176 </View> 173 177 )