Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
122
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 )