Split notification init, add cleanup (#2102)

* Inline never-imported export

* Remove pretense at handling cleanup

* Extract per-session listener to a function

* Split notifications.init() from notifications.onSessionLoaded()

* Tweak forked code to be more similar

* Remove unnecessary guards

* Split notifications.onSessionLoaded in two

* Always use getAgent()

* Remove dep on listenSessionLoaded, add cleanup
zio/stable
dan 2023-12-06 18:05:59 +00:00 committed by GitHub
parent 7d158f82fb
commit de38595a7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 92 additions and 85 deletions

View File

@ -6,75 +6,80 @@ import {track} from 'lib/analytics/analytics'
import {logger} from '#/logger' import {logger} from '#/logger'
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
import {listenSessionLoaded} from '#/state/events' import {SessionAccount, getAgent} from '#/state/session'
const SERVICE_DID = (serviceUrl?: string) => const SERVICE_DID = (serviceUrl?: string) =>
serviceUrl?.includes('staging') serviceUrl?.includes('staging')
? 'did:web:api.staging.bsky.dev' ? 'did:web:api.staging.bsky.dev'
: 'did:web:api.bsky.app' : 'did:web:api.bsky.app'
export function init(queryClient: QueryClient) { export async function requestPermissionsAndRegisterToken(
listenSessionLoaded(async (account, agent) => { account: SessionAccount,
// request notifications permission once the user has logged in ) {
const perms = await Notifications.getPermissionsAsync() // request notifications permission once the user has logged in
if (!perms.granted) { const perms = await Notifications.getPermissionsAsync()
await Notifications.requestPermissionsAsync() if (!perms.granted) {
} await Notifications.requestPermissionsAsync()
}
// register the push token with the server // register the push token with the server
const token = await getPushToken() const token = await Notifications.getDevicePushTokenAsync()
if (token) { try {
try { await getAgent().api.app.bsky.notification.registerPush({
await agent.api.app.bsky.notification.registerPush({ serviceDid: SERVICE_DID(account.service),
serviceDid: SERVICE_DID(account.service), platform: devicePlatform,
platform: devicePlatform, token: token.data,
token: token.data, appId: 'xyz.blueskyweb.app',
appId: 'xyz.blueskyweb.app', })
}) logger.debug(
logger.debug( 'Notifications: Sent push token (init)',
'Notifications: Sent push token (init)', {
{ tokenType: token.type,
tokenType: token.type, token: token.data,
token: token.data, },
}, logger.DebugContext.notifications,
logger.DebugContext.notifications, )
) } catch (error) {
} catch (error) { logger.error('Notifications: Failed to set push token', {error})
logger.error('Notifications: Failed to set push token', {error}) }
} }
}
// listens for new changes to the push token export function registerTokenChangeHandler(
// In rare situations, a push token may be changed by the push notification service while the app is running. When a token is rolled, the old one becomes invalid and sending notifications to it will fail. A push token listener will let you handle this situation gracefully by registering the new token with your backend right away. account: SessionAccount,
Notifications.addPushTokenListener(async ({data: t, type}) => { ): () => void {
// listens for new changes to the push token
// In rare situations, a push token may be changed by the push notification service while the app is running. When a token is rolled, the old one becomes invalid and sending notifications to it will fail. A push token listener will let you handle this situation gracefully by registering the new token with your backend right away.
const sub = Notifications.addPushTokenListener(async newToken => {
logger.debug(
'Notifications: Push token changed',
{tokenType: newToken.data, token: newToken.type},
logger.DebugContext.notifications,
)
try {
await getAgent().api.app.bsky.notification.registerPush({
serviceDid: SERVICE_DID(account.service),
platform: devicePlatform,
token: newToken.data,
appId: 'xyz.blueskyweb.app',
})
logger.debug( logger.debug(
'Notifications: Push token changed', 'Notifications: Sent push token (event)',
{t, tokenType: type}, {
tokenType: newToken.type,
token: newToken.data,
},
logger.DebugContext.notifications, logger.DebugContext.notifications,
) )
if (t) { } catch (error) {
try { logger.error('Notifications: Failed to set push token', {error})
await agent.api.app.bsky.notification.registerPush({ }
serviceDid: SERVICE_DID(account.service),
platform: devicePlatform,
token: t,
appId: 'xyz.blueskyweb.app',
})
logger.debug(
'Notifications: Sent push token (event)',
{
tokenType: type,
token: t,
},
logger.DebugContext.notifications,
)
} catch (error) {
logger.error('Notifications: Failed to set push token', {error})
}
}
})
}) })
return () => {
sub.remove()
}
}
export function init(queryClient: QueryClient) {
// handle notifications that are received, both in the foreground or background // handle notifications that are received, both in the foreground or background
Notifications.addNotificationReceivedListener(event => { Notifications.addNotificationReceivedListener(event => {
logger.debug( logger.debug(
@ -104,35 +109,23 @@ export function init(queryClient: QueryClient) {
}) })
// handle notifications that are tapped on // handle notifications that are tapped on
const sub = Notifications.addNotificationResponseReceivedListener( Notifications.addNotificationResponseReceivedListener(response => {
response => { logger.debug(
'Notifications: response received',
{
actionIdentifier: response.actionIdentifier,
},
logger.DebugContext.notifications,
)
if (response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER) {
logger.debug( logger.debug(
'Notifications: response received', 'User pressed a notification, opening notifications tab',
{ {},
actionIdentifier: response.actionIdentifier,
},
logger.DebugContext.notifications, logger.DebugContext.notifications,
) )
if ( track('Notificatons:OpenApp')
response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
) { resetToTab('NotificationsTab') // open notifications tab
logger.debug( }
'User pressed a notification, opening notifications tab', })
{},
logger.DebugContext.notifications,
)
track('Notificatons:OpenApp')
truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
resetToTab('NotificationsTab') // open notifications tab
}
},
)
return () => {
sub.remove()
}
}
export function getPushToken() {
return Notifications.getDevicePushTokenAsync()
} }

View File

@ -31,6 +31,7 @@ import {
import {isAndroid} from 'platform/detection' import {isAndroid} from 'platform/detection'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useCloseAnyActiveElement} from '#/state/util' import {useCloseAnyActiveElement} from '#/state/util'
import * as notifications from 'lib/notifications/notifications'
function ShellInner() { function ShellInner() {
const isDrawerOpen = useIsDrawerOpen() const isDrawerOpen = useIsDrawerOpen()
@ -52,7 +53,7 @@ function ShellInner() {
[setIsDrawerOpen], [setIsDrawerOpen],
) )
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
const {hasSession} = useSession() const {hasSession, currentAccount} = useSession()
const closeAnyActiveElement = useCloseAnyActiveElement() const closeAnyActiveElement = useCloseAnyActiveElement()
React.useEffect(() => { React.useEffect(() => {
@ -67,6 +68,19 @@ function ShellInner() {
} }
}, [closeAnyActiveElement]) }, [closeAnyActiveElement])
React.useEffect(() => {
if (currentAccount) {
notifications.requestPermissionsAndRegisterToken(currentAccount)
}
}, [currentAccount])
React.useEffect(() => {
if (currentAccount) {
const unsub = notifications.registerTokenChangeHandler(currentAccount)
return unsub
}
}, [currentAccount])
return ( return (
<> <>
<View style={containerPadding}> <View style={containerPadding}>