Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at 6982eb4fb4d44105dc8b44e898d452d4e5d32c82 325 lines 9.8 kB view raw
1import {useCallback, useEffect} from 'react' 2import {Platform} from 'react-native' 3import * as Notifications from 'expo-notifications' 4import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications' 5import {type AtpAgent} from '@atproto/api' 6import {type AppBskyNotificationRegisterPush} from '@atproto/api' 7import debounce from 'lodash.debounce' 8 9import { 10 BLUESKY_NOTIF_SERVICE_HEADERS, 11 PUBLIC_APPVIEW_DID, 12 PUBLIC_STAGING_APPVIEW_DID, 13} from '#/lib/constants' 14import {logger as notyLogger} from '#/lib/notifications/util' 15import {isNetworkError} from '#/lib/strings/errors' 16import {type SessionAccount, useAgent, useSession} from '#/state/session' 17import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' 18import {useAgeAssurance} from '#/ageAssurance' 19import {IS_NATIVE} from '#/env' 20import {IS_DEV} from '#/env' 21 22/** 23 * @private 24 * Registers the device's push notification token with the Bluesky server. 25 */ 26async function _registerPushToken({ 27 agent, 28 currentAccount, 29 token, 30 // extra = {}, 31}: { 32 agent: AtpAgent 33 currentAccount: SessionAccount 34 token: Notifications.DevicePushToken 35 extra?: { 36 ageRestricted?: boolean 37 } 38}) { 39 try { 40 const payload: AppBskyNotificationRegisterPush.InputSchema = { 41 serviceDid: currentAccount.service?.includes('staging') 42 ? PUBLIC_STAGING_APPVIEW_DID 43 : PUBLIC_APPVIEW_DID, 44 platform: Platform.OS, 45 token: token.data, 46 appId: 'app.witchsky', 47 // ageRestricted: extra.ageRestricted ?? false, 48 } 49 50 notyLogger.debug(`registerPushToken: registering`, {...payload}) 51 52 await agent.app.bsky.notification.registerPush(payload, { 53 headers: BLUESKY_NOTIF_SERVICE_HEADERS, 54 }) 55 56 notyLogger.debug(`registerPushToken: success`) 57 } catch (error) { 58 if (!isNetworkError(error)) { 59 notyLogger.error(`registerPushToken: failed`, {safeMessage: error}) 60 } 61 } 62} 63 64/** 65 * @private 66 * Debounced version of `_registerPushToken` to prevent multiple calls. 67 */ 68const _registerPushTokenDebounced = debounce(_registerPushToken, 100) 69 70/** 71 * Hook to register the device's push notification token with the Bluesky. If 72 * the user is not logged in, this will do nothing. 73 * 74 * Use this instead of using `_registerPushToken` or 75 * `_registerPushTokenDebounced` directly. 76 */ 77export function useRegisterPushToken() { 78 const agent = useAgent() 79 const {currentAccount} = useSession() 80 81 return useCallback( 82 ({ 83 token, 84 isAgeRestricted, 85 }: { 86 token: Notifications.DevicePushToken 87 isAgeRestricted: boolean 88 }) => { 89 if (!currentAccount) return 90 return _registerPushTokenDebounced({ 91 agent, 92 currentAccount, 93 token, 94 extra: { 95 ageRestricted: isAgeRestricted, 96 }, 97 }) 98 }, 99 [agent, currentAccount], 100 ) 101} 102 103/** 104 * Retreive the device's push notification token, if permissions are granted. 105 */ 106async function getPushToken() { 107 const granted = (await Notifications.getPermissionsAsync()).granted 108 notyLogger.debug(`getPushToken`, {granted}) 109 if (granted) { 110 return Notifications.getDevicePushTokenAsync() 111 } 112} 113 114/** 115 * Hook to get the device push token and register it with the Bluesky server. 116 * Should only be called after a user has logged-in, since registration is an 117 * authed endpoint. 118 * 119 * N.B. A previous regression in `expo-notifications` caused 120 * `addPushTokenListener` to not fire on Android after calling 121 * `getPushToken()`. Therefore, as insurance, we also call 122 * `registerPushToken` here. 123 * 124 * Because `registerPushToken` is debounced, even if the the listener _does_ 125 * fire, it's OK to also call `registerPushToken` below since only a single 126 * call will be made to the server (ideally). This does race the listener (if 127 * it fires), so there's a possibility that multiple calls will be made, but 128 * that is acceptable. 129 * 130 * @see https://github.com/expo/expo/issues/28656 131 * @see https://github.com/expo/expo/issues/29909 132 * @see https://github.com/bluesky-social/social-app/pull/4467 133 */ 134export function useGetAndRegisterPushToken() { 135 const aa = useAgeAssurance() 136 const registerPushToken = useRegisterPushToken() 137 return useCallback( 138 async ({ 139 isAgeRestricted: isAgeRestrictedOverride, 140 }: { 141 isAgeRestricted?: boolean 142 } = {}) => { 143 if (!IS_NATIVE || IS_DEV) return 144 145 /** 146 * This will also fire the listener added via `addPushTokenListener`. That 147 * listener also handles registration. 148 */ 149 const token = await getPushToken() 150 151 notyLogger.debug(`useGetAndRegisterPushToken`, { 152 token: token ?? 'undefined', 153 }) 154 155 if (token) { 156 /** 157 * The listener should have registered the token already, but just in 158 * case, call the debounced function again. 159 */ 160 registerPushToken({ 161 token, 162 isAgeRestricted: 163 isAgeRestrictedOverride ?? aa.state.access !== aa.Access.Full, 164 }) 165 } 166 167 return token 168 }, 169 [registerPushToken, aa], 170 ) 171} 172 173/** 174 * Hook to register the device's push notification token with the Bluesky 175 * server, as well as listen for push token updates, should they occurr. 176 * 177 * Registered via the shell, which wraps the navigation stack, meaning if we 178 * have a current account, this handling will be registered and ready to go. 179 */ 180export function useNotificationsRegistration() { 181 const {currentAccount} = useSession() 182 const registerPushToken = useRegisterPushToken() 183 const getAndRegisterPushToken = useGetAndRegisterPushToken() 184 const aa = useAgeAssurance() 185 186 useEffect(() => { 187 /** 188 * We want this to init right away _after_ we have a logged in user, and 189 * _after_ we've loaded their age assurance state. 190 */ 191 if (!currentAccount) return 192 193 notyLogger.debug(`useNotificationsRegistration`) 194 195 /** 196 * Init push token, if permissions are granted already. If they weren't, 197 * they'll be requested by the `useRequestNotificationsPermission` hook 198 * below. 199 */ 200 getAndRegisterPushToken() 201 202 /** 203 * Register the push token with the Bluesky server, whenever it changes. 204 * This is also fired any time `getDevicePushTokenAsync` is called. 205 * 206 * Since this is registered immediately after `getAndRegisterPushToken`, it 207 * should also detect that getter and be fired almost immediately after this. 208 * 209 * According to the Expo docs, there is a chance that the token will change 210 * while the app is open in some rare cases. This will fire 211 * `registerPushToken` whenever that happens. 212 * 213 * @see https://docs.expo.dev/versions/latest/sdk/notifications/#addpushtokenlistenerlistener 214 */ 215 const subscription = Notifications.addPushTokenListener(async token => { 216 registerPushToken({ 217 token, 218 isAgeRestricted: aa.state.access !== aa.Access.Full, 219 }) 220 notyLogger.debug(`addPushTokenListener callback`, {token}) 221 }) 222 223 return () => { 224 subscription.remove() 225 } 226 }, [currentAccount, getAndRegisterPushToken, registerPushToken, aa]) 227} 228 229export function useRequestNotificationsPermission() { 230 const {currentAccount} = useSession() 231 const getAndRegisterPushToken = useGetAndRegisterPushToken() 232 233 return async ( 234 context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home', 235 ) => { 236 const permissions = await Notifications.getPermissionsAsync() 237 238 if ( 239 !IS_NATIVE || 240 permissions?.status === 'granted' || 241 (permissions?.status === 'denied' && !permissions.canAskAgain) 242 ) { 243 return 244 } 245 if (context === 'AfterOnboarding') { 246 return 247 } 248 if (context === 'Home' && !currentAccount) { 249 return 250 } 251 252 const res = await Notifications.requestPermissionsAsync() 253 254 notyLogger.metric(`notifications:request`, { 255 context: context, 256 status: res.status, 257 }) 258 259 if (res.granted) { 260 if (currentAccount) { 261 /** 262 * If we have an account in scope, we can safely call 263 * `getAndRegisterPushToken`. 264 */ 265 getAndRegisterPushToken() 266 } else { 267 /** 268 * Right after login, `currentAccount` in this scope will be undefined, 269 * but calling `getPushToken` will result in `addPushTokenListener` 270 * listeners being called, which will handle the registration with the 271 * Bluesky server. 272 */ 273 getPushToken() 274 } 275 } 276 } 277} 278 279export async function decrementBadgeCount(by: number) { 280 if (!IS_NATIVE) return 281 282 let count = await getBadgeCountAsync() 283 count -= by 284 if (count < 0) { 285 count = 0 286 } 287 288 await BackgroundNotificationHandler.setBadgeCountAsync(count) 289 await setBadgeCountAsync(count) 290} 291 292export async function resetBadgeCount() { 293 await BackgroundNotificationHandler.setBadgeCountAsync(0) 294 await setBadgeCountAsync(0) 295} 296 297export async function unregisterPushToken(agents: AtpAgent[]) { 298 if (!IS_NATIVE) return 299 300 try { 301 const token = await getPushToken() 302 if (token) { 303 for (const agent of agents) { 304 await agent.app.bsky.notification.unregisterPush( 305 { 306 serviceDid: agent.serviceUrl.hostname.includes('staging') 307 ? PUBLIC_STAGING_APPVIEW_DID 308 : PUBLIC_APPVIEW_DID, 309 platform: Platform.OS, 310 token: token.data, 311 appId: 'xyz.blueskyweb.app', 312 }, 313 { 314 headers: BLUESKY_NOTIF_SERVICE_HEADERS, 315 }, 316 ) 317 notyLogger.debug(`Push token unregistered for ${agent.session?.handle}`) 318 } 319 } else { 320 notyLogger.debug('Tried to unregister push token, but could not find one') 321 } 322 } catch (error) { 323 notyLogger.debug('Failed to unregister push token', {message: error}) 324 } 325}