mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

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