···11-import React from 'react'
11+import {useCallback, useEffect} from 'react'
22+import {Platform} from 'react-native'
23import * as Notifications from 'expo-notifications'
34import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications'
44-import {type BskyAgent} from '@atproto/api'
55+import {type AtpAgent} from '@atproto/api'
66+import debounce from 'lodash.debounce'
5766-import {logEvent} from '#/lib/statsig/statsig'
88+import {PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID} from '#/lib/constants'
79import {Logger} from '#/logger'
88-import {devicePlatform, isAndroid, isNative} from '#/platform/detection'
1010+import {isNative} from '#/platform/detection'
911import {type SessionAccount, useAgent, useSession} from '#/state/session'
1010-import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'
1111-1212-const SERVICE_DID = (serviceUrl?: string) =>
1313- serviceUrl?.includes('staging')
1414- ? 'did:web:api.staging.bsky.dev'
1515- : 'did:web:api.bsky.app'
1212+import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
16131714const logger = Logger.create(Logger.Context.Notifications)
18151919-async function registerPushToken(
2020- agent: BskyAgent,
2121- account: SessionAccount,
2222- token: Notifications.DevicePushToken,
2323-) {
1616+/**
1717+ * @private
1818+ * Registers the device's push notification token with the Bluesky server.
1919+ */
2020+async function _registerPushToken({
2121+ agent,
2222+ currentAccount,
2323+ token,
2424+}: {
2525+ agent: AtpAgent
2626+ currentAccount: SessionAccount
2727+ token: Notifications.DevicePushToken
2828+}) {
2429 try {
2525- await agent.api.app.bsky.notification.registerPush({
2626- serviceDid: SERVICE_DID(account.service),
2727- platform: devicePlatform,
3030+ await agent.app.bsky.notification.registerPush({
3131+ serviceDid: currentAccount.service?.includes('staging')
3232+ ? PUBLIC_STAGING_APPVIEW_DID
3333+ : PUBLIC_APPVIEW_DID,
3434+ platform: Platform.OS,
2835 token: token.data,
2936 appId: 'social.deer',
3037 })
3131- logger.debug('Notifications: Sent push token (init)', {
3838+3939+ logger.debug(`registerPushToken: success`, {
3240 tokenType: token.type,
3341 token: token.data,
3442 })
3543 } catch (error) {
3636- logger.error('Notifications: Failed to set push token', {message: error})
4444+ logger.error(`registerPushToken: failed`, {safeMessage: error})
3745 }
3846}
39474040-async function getPushToken(skipPermissionCheck = false) {
4141- const granted =
4242- skipPermissionCheck || (await Notifications.getPermissionsAsync()).granted
4848+/**
4949+ * @private
5050+ * Debounced version of `_registerPushToken` to prevent multiple calls.
5151+ */
5252+const _registerPushTokenDebounced = debounce(_registerPushToken, 100)
5353+5454+/**
5555+ * Hook to register the device's push notification token with the Bluesky. If
5656+ * the user is not logged in, this will do nothing.
5757+ *
5858+ * Use this instead of using `_registerPushToken` or
5959+ * `_registerPushTokenDebounced` directly.
6060+ */
6161+export function useRegisterPushToken() {
6262+ const agent = useAgent()
6363+ const {currentAccount} = useSession()
6464+6565+ return useCallback(
6666+ ({token}: {token: Notifications.DevicePushToken}) => {
6767+ if (!currentAccount) return
6868+ return _registerPushTokenDebounced({
6969+ agent,
7070+ currentAccount,
7171+ token,
7272+ })
7373+ },
7474+ [agent, currentAccount],
7575+ )
7676+}
7777+7878+/**
7979+ * Retreive the device's push notification token, if permissions are granted.
8080+ */
8181+async function getPushToken() {
8282+ const granted = (await Notifications.getPermissionsAsync()).granted
8383+ logger.debug(`getPushToken`, {granted})
4384 if (granted) {
4485 return Notifications.getDevicePushTokenAsync()
4586 }
4687}
47888989+/**
9090+ * Hook to get the device push token and register it with the Bluesky server.
9191+ * Should only be called after a user has logged-in, since registration is an
9292+ * authed endpoint.
9393+ *
9494+ * N.B. A previous regression in `expo-notifications` caused
9595+ * `addPushTokenListener` to not fire on Android after calling
9696+ * `getPushToken()`. Therefore, as insurance, we also call
9797+ * `registerPushToken` here.
9898+ *
9999+ * Because `registerPushToken` is debounced, even if the the listener _does_
100100+ * fire, it's OK to also call `registerPushToken` below since only a single
101101+ * call will be made to the server (ideally). This does race the listener (if
102102+ * it fires), so there's a possibility that multiple calls will be made, but
103103+ * that is acceptable.
104104+ *
105105+ * @see https://github.com/bluesky-social/social-app/pull/4467
106106+ * @see https://github.com/expo/expo/issues/28656
107107+ * @see https://github.com/expo/expo/issues/29909
108108+ */
109109+export function useGetAndRegisterPushToken() {
110110+ const registerPushToken = useRegisterPushToken()
111111+ return useCallback(async () => {
112112+ /**
113113+ * This will also fire the listener added via `addPushTokenListener`. That
114114+ * listener also handles registration.
115115+ */
116116+ const token = await getPushToken()
117117+118118+ logger.debug(`useGetAndRegisterPushToken`, {token: token ?? 'undefined'})
119119+120120+ if (token) {
121121+ /**
122122+ * The listener should have registered the token already, but just in
123123+ * case, call the debounced function again.
124124+ */
125125+ registerPushToken({token})
126126+ }
127127+128128+ return token
129129+ }, [registerPushToken])
130130+}
131131+132132+/**
133133+ * Hook to register the device's push notification token with the Bluesky
134134+ * server, as well as listen for push token updates, should they occurr.
135135+ *
136136+ * Registered via the shell, which wraps the navigation stack, meaning if we
137137+ * have a current account, this handling will be registered and ready to go.
138138+ */
48139export function useNotificationsRegistration() {
4949- const agent = useAgent()
50140 const {currentAccount} = useSession()
141141+ const registerPushToken = useRegisterPushToken()
142142+ const getAndRegisterPushToken = useGetAndRegisterPushToken()
511435252- React.useEffect(() => {
5353- if (!currentAccount) {
5454- return
5555- }
144144+ useEffect(() => {
145145+ /**
146146+ * We want this to init right away _after_ we have a logged in user.
147147+ */
148148+ if (!currentAccount) return
561495757- // HACK - see https://github.com/bluesky-social/social-app/pull/4467
5858- // An apparent regression in expo-notifications causes `addPushTokenListener` to not fire on Android whenever the
5959- // token changes by calling `getPushToken()`. This is a workaround to ensure we register the token once it is
6060- // generated on Android.
6161- if (isAndroid) {
6262- ;(async () => {
6363- const token = await getPushToken()
150150+ logger.debug(`useNotificationsRegistration`)
641516565- // Token will be undefined if we don't have notifications permission
6666- if (token) {
6767- registerPushToken(agent, currentAccount, token)
6868- }
6969- })()
7070- } else {
7171- getPushToken()
7272- }
152152+ /**
153153+ * Init push token, if permissions are granted already. If they weren't,
154154+ * they'll be requested by the `useRequestNotificationsPermission` hook
155155+ * below.
156156+ */
157157+ getAndRegisterPushToken()
731587474- // According to the Expo docs, there is a chance that the token will change while the app is open in some rare
7575- // cases. This will fire `registerPushToken` whenever that happens.
7676- const subscription = Notifications.addPushTokenListener(async newToken => {
7777- registerPushToken(agent, currentAccount, newToken)
159159+ /**
160160+ * Register the push token with the Bluesky server, whenever it changes.
161161+ * This is also fired any time `getDevicePushTokenAsync` is called.
162162+ *
163163+ * According to the Expo docs, there is a chance that the token will change
164164+ * while the app is open in some rare cases. This will fire
165165+ * `registerPushToken` whenever that happens.
166166+ *
167167+ * @see https://docs.expo.dev/versions/latest/sdk/notifications/#addpushtokenlistenerlistener
168168+ */
169169+ const subscription = Notifications.addPushTokenListener(async token => {
170170+ registerPushToken({token})
171171+ logger.debug(`addPushTokenListener callback`, {token})
78172 })
7917380174 return () => {
81175 subscription.remove()
82176 }
8383- }, [currentAccount, agent])
177177+ }, [currentAccount, getAndRegisterPushToken, registerPushToken])
84178}
8517986180export function useRequestNotificationsPermission() {
87181 const {currentAccount} = useSession()
8888- const agent = useAgent()
182182+ const getAndRegisterPushToken = useGetAndRegisterPushToken()
8918390184 return async (
91185 context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home',
···107201 }
108202109203 const res = await Notifications.requestPermissionsAsync()
110110- logEvent('notifications:request', {
204204+205205+ logger.metric(`notifications:request`, {
111206 context: context,
112207 status: res.status,
113208 })
114209115210 if (res.granted) {
116116- // This will fire a pushTokenEvent, which will handle registration of the token
117117- const token = await getPushToken(true)
118118-119119- // Same hack as above. We cannot rely on the `addPushTokenListener` to fire on Android due to an Expo bug, so we
120120- // will manually register it here. Note that this will occur only:
121121- // 1. right after the user signs in, leading to no `currentAccount` account being available - this will be instead
122122- // picked up from the useEffect above on `currentAccount` change
123123- // 2. right after onboarding. In this case, we _need_ this registration, since `currentAccount` will not change
124124- // and we need to ensure the token is registered right after permission is granted. `currentAccount` will already
125125- // be available in this case, so the registration will succeed.
126126- // We should remove this once expo-notifications (and possibly FCMv1) is fixed and the `addPushTokenListener` is
127127- // working again. See https://github.com/expo/expo/issues/28656
128128- if (isAndroid && currentAccount && token) {
129129- registerPushToken(agent, currentAccount, token)
211211+ if (currentAccount) {
212212+ /**
213213+ * If we have an account in scope, we can safely call
214214+ * `getAndRegisterPushToken`.
215215+ */
216216+ getAndRegisterPushToken()
217217+ } else {
218218+ /**
219219+ * Right after login, `currentAccount` in this scope will be undefined,
220220+ * but calling `getPushToken` will result in `addPushTokenListener`
221221+ * listeners being called, which will handle the registration with the
222222+ * Bluesky server.
223223+ */
224224+ getPushToken()
130225 }
131226 }
132227 }
+24-22
src/locale/locales/en/messages.po
···456456msgid "<0>{0}</0> is included in your starter pack"
457457msgstr ""
458458459459-#: src/components/WhoCanReply.tsx:302
459459+#: src/components/WhoCanReply.tsx:317
460460msgid "<0>{0}</0> members"
461461msgstr ""
462462···939939msgid "an unknown labeler"
940940msgstr ""
941941942942-#: src/components/WhoCanReply.tsx:323
942942+#: src/components/WhoCanReply.tsx:338
943943msgid "and"
944944msgstr ""
945945···12761276msgid "Blocked post."
12771277msgstr ""
1278127812791279-#: src/screens/Profile/Sections/Labels.tsx:170
12791279+#: src/screens/Profile/Sections/Labels.tsx:203
12801280msgid "Blocking does not prevent this labeler from placing labels on your account."
12811281msgstr ""
12821282···17411741#: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:123
17421742#: src/components/verification/VerificationsDialog.tsx:144
17431743#: src/components/verification/VerifierDialog.tsx:144
17441744+#: src/components/WhoCanReply.tsx:200
17451745+#: src/components/WhoCanReply.tsx:207
17441746#: src/view/com/modals/ChangePassword.tsx:279
17451747#: src/view/com/modals/ChangePassword.tsx:282
17461748msgid "Close"
···24332435msgid "Developer options"
24342436msgstr ""
2435243724362436-#: src/components/WhoCanReply.tsx:185
24382438+#: src/components/WhoCanReply.tsx:186
24372439msgid "Dialog: adjust who can interact with this post"
24382440msgstr ""
24392441···29882990msgid "Everybody can reply"
29892991msgstr ""
2990299229912991-#: src/components/WhoCanReply.tsx:228
29932993+#: src/components/WhoCanReply.tsx:243
29922994msgid "Everybody can reply to this post."
29932995msgstr ""
29942996···30583060msgid "Expands or collapses post text"
30593061msgstr ""
3060306230613061-#: src/lib/api/index.ts:406
30633063+#: src/lib/api/index.ts:418
30623064msgid "Expected uri to resolve to a record"
30633065msgstr ""
30643066···42504252msgid "Labels added"
42514253msgstr ""
4252425442534253-#: src/screens/Profile/Sections/Labels.tsx:160
42554255+#: src/screens/Profile/Sections/Labels.tsx:194
42544256msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network."
42554257msgstr ""
42564258···43564358msgstr ""
4357435943584360#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:84
43594359-msgid "Leave them all unchecked to see any language."
43614361+msgid "Leave them all unselected to see any language."
43604362msgstr ""
4361436343624364#: src/components/dialogs/LinkWarning.tsx:67
···46804682msgid "Mention notifications"
46814683msgstr ""
4682468446834683-#: src/components/WhoCanReply.tsx:269
46854685+#: src/components/WhoCanReply.tsx:284
46844686msgid "mentioned users"
46854687msgstr ""
46864688···51935195msgid "No one"
51945196msgstr ""
5195519751965196-#: src/components/WhoCanReply.tsx:252
51985198+#: src/components/WhoCanReply.tsx:267
51975199msgid "No one but the author can quote this post."
51985200msgstr ""
51995201···54195421msgid "Only .jpg and .png files are supported"
54205422msgstr ""
5421542354225422-#: src/components/WhoCanReply.tsx:232
54245424+#: src/components/WhoCanReply.tsx:247
54235425msgid "Only {0} can reply."
54245426msgstr ""
54255427···65216523msgid "Replies disabled"
65226524msgstr ""
6523652565246524-#: src/components/WhoCanReply.tsx:230
65266526+#: src/components/WhoCanReply.tsx:245
65256527msgid "Replies to this post are disabled."
65266528msgstr ""
65276529···7674767676757677#: src/components/ReportDialog/index.tsx:54
76767678#: src/screens/Moderation/index.tsx:101
76777677-#: src/screens/Profile/Sections/Labels.tsx:121
76797679+#: src/screens/Profile/Sections/Labels.tsx:184
76787680msgid "Something went wrong, please try again."
76797681msgstr ""
76807682···76947696msgid "Something wrong? Let us know."
76957697msgstr ""
7696769876977697-#: src/App.native.tsx:122
76997699+#: src/App.native.tsx:123
76987700#: src/App.web.tsx:99
76997701msgid "Sorry! Your session expired. Please sign in again."
77007702msgstr ""
···78347836msgid "Subscribe"
78357837msgstr ""
7836783878377837-#: src/screens/Profile/Sections/Labels.tsx:198
78397839+#: src/screens/Profile/Sections/Labels.tsx:231
78387840msgid "Subscribe to @{0} to use these labels:"
78397841msgstr ""
78407842···83588360msgid "This label was applied by you."
83598361msgstr ""
8360836283618361-#: src/screens/Profile/Sections/Labels.tsx:185
83638363+#: src/screens/Profile/Sections/Labels.tsx:218
83628364msgid "This labeler hasn't declared what labels it publishes, and may not be active."
83638365msgstr ""
83648366···83878389msgid "This post claims to have been created on <0>{0}</0>, but was first seen by Bluesky on <1>{1}</1>."
83888390msgstr ""
8389839183908390-#: src/components/WhoCanReply.tsx:223
83928392+#: src/components/WhoCanReply.tsx:238
83918393msgid "This post has an unknown type of threadgate on it. Your app may be out of date."
83928394msgstr ""
83938395···88888890msgid "Uploading images..."
88898891msgstr ""
8890889288918891-#: src/lib/api/index.ts:356
88928892-#: src/lib/api/index.ts:380
88938893+#: src/lib/api/index.ts:368
88948894+#: src/lib/api/index.ts:392
88938895msgid "Uploading link thumbnail..."
88948896msgstr ""
88958897···89848986msgid "Username or email address"
89858987msgstr ""
8986898889878987-#: src/components/WhoCanReply.tsx:286
89898989+#: src/components/WhoCanReply.tsx:301
89888990msgid "users followed by <0>@{0}</0>"
89898991msgstr ""
8990899289918991-#: src/components/WhoCanReply.tsx:273
89938993+#: src/components/WhoCanReply.tsx:288
89928994msgid "users following <0>@{0}</0>"
89938995msgstr ""
89948996···94169418msgid "Which languages would you like to see in your algorithmic feeds?"
94179419msgstr ""
9418942094199419-#: src/components/WhoCanReply.tsx:189
94219421+#: src/components/WhoCanReply.tsx:190
94209422msgid "Who can interact with this post?"
94219423msgstr ""
94229424
···8181 </Trans>
8282 </Text>
8383 <Text style={[pal.textLight, styles.description]}>
8484- <Trans>Leave them all unchecked to see any language.</Trans>
8484+ <Trans>Leave them all unselected to see any language.</Trans>
8585 </Text>
8686 <ScrollView style={styles.scrollContainer}>
8787 {languages.map(lang => (
+4-4
yarn.lock
···1685516855 resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz#4bba0f5f04e2cf222494cce3a9794ba6a4894dee"
1685616856 integrity sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg==
16857168571685816858-react-native-pager-view@^6.7.1:
1685916859- version "6.8.1"
1686016860- resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.8.1.tgz#fa0ec09ea7c44190c7c013d75dd09fdc17b96100"
1686116861- integrity sha512-XIyVEMhwq7sZqM7GobOJZXxFCfdFgVNq/CFB2rZIRNRSVPJqE1k1fsc8xfQKfdzsp6Rpt6I7VOIvhmP7/YHdVg==
1685816858+react-native-pager-view@6.8.0:
1685916859+ version "6.8.0"
1686016860+ resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.8.0.tgz#5bac05203d911bf9bf039d47db41b1313dbd1a7a"
1686116861+ integrity sha512-/wgFV8nB4TLnQ6j9e3TvNrHbPF5TINMZHaXt86GOV0NSJNMVGkWguniJVKrYLm85LL8KVhRkgdh43Rdu7PvW1A==
16862168621686316863react-native-progress@bluesky-social/react-native-progress:
1686416864 version "5.0.0"