Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 231 lines 7.1 kB view raw
1import {useCallback, useEffect, useRef, useState} from 'react' 2import {Alert, AppState, type AppStateStatus} from 'react-native' 3import {nativeBuildVersion} from 'expo-application' 4import { 5 checkForUpdateAsync, 6 fetchUpdateAsync, 7 isEnabled, 8 reloadAsync, 9 setExtraParamAsync, 10 useUpdates, 11} from 'expo-updates' 12 13import {isNetworkError} from '#/lib/strings/errors' 14import {logger} from '#/logger' 15import {IS_ANDROID, IS_IOS, IS_TESTFLIGHT} from '#/env' 16 17const MINIMUM_MINIMIZE_TIME = 15 * 60e3 18 19async function setExtraParams() { 20 await setExtraParamAsync( 21 IS_IOS ? 'ios-build-number' : 'android-build-number', 22 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. 23 // This just ensures it gets passed as a string 24 `${nativeBuildVersion}`, 25 ) 26 await setExtraParamAsync( 27 'channel', 28 IS_TESTFLIGHT ? 'testflight' : 'production', 29 ) 30} 31 32async function setExtraParamsPullRequest(channel: string) { 33 await setExtraParamAsync( 34 IS_IOS ? 'ios-build-number' : 'android-build-number', 35 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. 36 // This just ensures it gets passed as a string 37 `${nativeBuildVersion}`, 38 ) 39 await setExtraParamAsync('channel', channel) 40} 41 42async function updateTestflight() { 43 await setExtraParams() 44 45 const res = await checkForUpdateAsync() 46 if (res.isAvailable) { 47 await fetchUpdateAsync() 48 Alert.alert( 49 'Update Available', 50 'A new version of the app is available. Relaunch now?', 51 [ 52 { 53 text: 'No', 54 style: 'cancel', 55 }, 56 { 57 text: 'Relaunch', 58 style: 'default', 59 onPress: async () => { 60 await reloadAsync() 61 }, 62 }, 63 ], 64 ) 65 } 66} 67 68export function useApplyPullRequestOTAUpdate() { 69 const {currentlyRunning} = useUpdates() 70 const [pending, setPending] = useState(false) 71 const currentChannel = currentlyRunning?.channel 72 const isCurrentlyRunningPullRequestDeployment = 73 currentChannel?.startsWith('pull-request') 74 75 const tryApplyUpdate = async (channel: string) => { 76 setPending(true) 77 await setExtraParamsPullRequest(channel) 78 const res = await checkForUpdateAsync() 79 if (res.isAvailable) { 80 Alert.alert( 81 'Deployment Available', 82 `A deployment of ${channel} is availalble. Applying this deployment may result in a bricked installation, in which case you will need to reinstall the app and may lose local data. Are you sure you want to proceed?`, 83 [ 84 { 85 text: 'No', 86 style: 'cancel', 87 }, 88 { 89 text: 'Relaunch', 90 style: 'default', 91 onPress: async () => { 92 await fetchUpdateAsync() 93 await reloadAsync() 94 }, 95 }, 96 ], 97 ) 98 } else { 99 Alert.alert( 100 'No Deployment Available', 101 `No new deployments of ${channel} are currently available for your current native build.`, 102 ) 103 } 104 setPending(false) 105 } 106 107 const revertToEmbedded = async () => { 108 try { 109 await updateTestflight() 110 } catch (e: any) { 111 logger.error('Internal OTA Update Error', {error: `${e}`}) 112 } 113 } 114 115 return { 116 tryApplyUpdate, 117 revertToEmbedded, 118 isCurrentlyRunningPullRequestDeployment, 119 currentChannel, 120 pending, 121 } 122} 123 124export function useOTAUpdates() { 125 const shouldReceiveUpdates = isEnabled && !__DEV__ 126 127 const appState = useRef<AppStateStatus>('active') 128 const lastMinimize = useRef(0) 129 const ranInitialCheck = useRef(false) 130 const timeout = useRef<NodeJS.Timeout>(undefined) 131 const {currentlyRunning, isUpdatePending} = useUpdates() 132 const currentChannel = currentlyRunning?.channel 133 134 const setCheckTimeout = useCallback(() => { 135 timeout.current = setTimeout(async () => { 136 try { 137 await setExtraParams() 138 139 logger.debug('Checking for update...') 140 const res = await checkForUpdateAsync() 141 142 if (res.isAvailable) { 143 logger.debug('Attempting to fetch update...') 144 await fetchUpdateAsync() 145 } else { 146 logger.debug('No update available.') 147 } 148 } catch (err) { 149 if (!isNetworkError(err)) { 150 logger.error('OTA Update Error', {safeMessage: err}) 151 } 152 } 153 }, 10e3) 154 }, []) 155 156 const onIsTestFlight = useCallback(async () => { 157 try { 158 await updateTestflight() 159 } catch (err: any) { 160 if (!isNetworkError(err)) { 161 logger.error('Internal OTA Update Error', {safeMessage: err}) 162 } 163 } 164 }, []) 165 166 useEffect(() => { 167 // We don't need to check anything if the current update is a PR update 168 if (currentChannel?.startsWith('pull-request')) { 169 return 170 } 171 172 // We use this setTimeout to allow analytics to initialize before we check for an update 173 // For Testflight users, we can prompt the user to update immediately whenever there's an available update. This 174 // is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update 175 // immediately. 176 if (IS_TESTFLIGHT) { 177 onIsTestFlight() 178 return 179 } else if (!shouldReceiveUpdates || ranInitialCheck.current) { 180 return 181 } 182 183 setCheckTimeout() 184 ranInitialCheck.current = true 185 }, [onIsTestFlight, currentChannel, setCheckTimeout, shouldReceiveUpdates]) 186 187 // After the app has been minimized for 15 minutes, we want to either A. install an update if one has become available 188 // or B check for an update again. 189 useEffect(() => { 190 // We also don't start this timeout if the user is on a pull request update 191 if (!isEnabled || currentChannel?.startsWith('pull-request')) { 192 return 193 } 194 195 // TEMP: disable wake-from-background OTA loading on Android. 196 // This is causing a crash when the thread view is open due to 197 // `maintainVisibleContentPosition`. See repro repo for more details: 198 // https://github.com/mozzius/ota-crash-repro 199 // Old Arch only - re-enable once we're on the New Archictecture! -sfn 200 if (IS_ANDROID) return 201 202 const subscription = AppState.addEventListener( 203 'change', 204 async nextAppState => { 205 if ( 206 appState.current.match(/inactive|background/) && 207 nextAppState === 'active' 208 ) { 209 // If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since 210 // chances are that there isn't anything important going on in the current session. 211 if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) { 212 if (isUpdatePending) { 213 await reloadAsync() 214 } else { 215 setCheckTimeout() 216 } 217 } 218 } else { 219 lastMinimize.current = Date.now() 220 } 221 222 appState.current = nextAppState 223 }, 224 ) 225 226 return () => { 227 clearTimeout(timeout.current) 228 subscription.remove() 229 } 230 }, [isUpdatePending, currentChannel, setCheckTimeout]) 231}