Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Merge branch 'main' of https://github.com/bluesky-social/social-app

+427 -276
+1 -1
package.json
··· 192 192 "react-native-ios-context-menu": "^1.15.3", 193 193 "react-native-keyboard-controller": "^1.17.5", 194 194 "react-native-mmkv": "^2.12.2", 195 - "react-native-pager-view": "^6.7.1", 195 + "react-native-pager-view": "6.8.0", 196 196 "react-native-progress": "bluesky-social/react-native-progress", 197 197 "react-native-qrcode-styled": "^0.3.3", 198 198 "react-native-reanimated": "~3.17.5",
+23 -8
src/components/WhoCanReply.tsx
··· 1 - import React from 'react' 1 + import {Fragment, useMemo} from 'react' 2 2 import { 3 3 Keyboard, 4 4 Platform, ··· 22 22 type ThreadgateAllowUISetting, 23 23 threadgateViewToAllowUISetting, 24 24 } from '#/state/queries/threadgate' 25 - import {atoms as a, useTheme} from '#/alf' 26 - import {Button} from '#/components/Button' 25 + import {atoms as a, useTheme, web} from '#/alf' 26 + import {Button, ButtonText} from '#/components/Button' 27 27 import * as Dialog from '#/components/Dialog' 28 28 import {useDialogControl} from '#/components/Dialog' 29 29 import { ··· 61 61 ) && post.record.reply?.root 62 62 ? post.record.reply.root.uri 63 63 : post.uri 64 - const settings = React.useMemo(() => { 64 + const settings = useMemo(() => { 65 65 return threadgateViewToAllowUISetting(post.threadgate) 66 66 }, [post.threadgate]) 67 67 ··· 178 178 embeddingDisabled: boolean 179 179 }) { 180 180 const {_} = useLingui() 181 + 181 182 return ( 182 - <Dialog.Outer control={control}> 183 + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 183 184 <Dialog.Handle /> 184 185 <Dialog.ScrollableInner 185 186 label={_(msg`Dialog: adjust who can interact with this post`)} 186 - style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}> 187 + style={web({maxWidth: 400})}> 187 188 <View style={[a.gap_sm]}> 188 189 <Text style={[a.font_bold, a.text_xl, a.pb_sm]}> 189 190 <Trans>Who can interact with this post?</Trans> ··· 194 195 embeddingDisabled={embeddingDisabled} 195 196 /> 196 197 </View> 198 + {isNative && ( 199 + <Button 200 + label={_(msg`Close`)} 201 + onPress={() => control.close()} 202 + size="small" 203 + variant="solid" 204 + color="secondary" 205 + style={[a.mt_5xl]}> 206 + <ButtonText> 207 + <Trans>Close</Trans> 208 + </ButtonText> 209 + </Button> 210 + )} 211 + <Dialog.Close /> 197 212 </Dialog.ScrollableInner> 198 213 </Dialog.Outer> 199 214 ) ··· 232 247 <Trans> 233 248 Only{' '} 234 249 {settings.map((rule, i) => ( 235 - <React.Fragment key={`rule-${i}`}> 250 + <Fragment key={`rule-${i}`}> 236 251 <Rule rule={rule} post={post} lists={post.threadgate!.lists} /> 237 252 <Separator i={i} length={settings.length} /> 238 - </React.Fragment> 253 + </Fragment> 239 254 ))}{' '} 240 255 can reply. 241 256 </Trans>
+2 -1
src/components/forms/TextField.tsx
··· 196 196 minWidth: 0, 197 197 }, 198 198 ios({paddingTop: 12, paddingBottom: 13}), 199 - android(a.py_md), 199 + // Needs to be sm on Paper, md on Fabric for some godforsaken reason -sfn 200 + android(a.py_sm), 200 201 // fix for autofill styles covering border 201 202 web({ 202 203 paddingTop: 10,
+16 -4
src/lib/api/index.ts
··· 333 333 return {lang: caption.lang, file: data.blob} 334 334 }), 335 335 ) 336 + 337 + // lexicon numbers must be floats 338 + const width = Math.round(videoDraft.asset.width) 339 + const height = Math.round(videoDraft.asset.height) 340 + 341 + // aspect ratio values must be >0 - better to leave as unset otherwise 342 + // posting will fail if aspect ratio is set to 0 343 + const aspectRatio = width > 0 && height > 0 ? {width, height} : undefined 344 + 345 + if (!aspectRatio) { 346 + logger.error( 347 + `Invalid aspect ratio - got { width: ${videoDraft.asset.width}, height: ${videoDraft.asset.height} }`, 348 + ) 349 + } 350 + 336 351 return { 337 352 $type: 'app.bsky.embed.video', 338 353 video: videoDraft.pendingPublish.blobRef, 339 354 alt: videoDraft.altText || undefined, 340 355 captions: captions.length === 0 ? undefined : captions, 341 - aspectRatio: { 342 - width: videoDraft.asset.width, 343 - height: videoDraft.asset.height, 344 - }, 356 + aspectRatio, 345 357 } 346 358 } 347 359 if (embedDraft.media?.type === 'gif') {
+3
src/lib/constants.ts
··· 190 190 }, 191 191 }, 192 192 } 193 + 194 + export const PUBLIC_APPVIEW_DID = 'did:web:api.bsky.app' 195 + export const PUBLIC_STAGING_APPVIEW_DID = 'did:web:api.staging.bsky.dev'
+5 -1
src/lib/hooks/useEmail.ts
··· 1 + import {STALE} from '#/state/queries' 1 2 import {useServiceConfigQuery} from '#/state/queries/email-verification-required' 2 3 import {useProfileQuery} from '#/state/queries/profile' 3 4 import {useSession} from '#/state/session' ··· 8 9 const {currentAccount} = useSession() 9 10 10 11 const {data: serviceConfig} = useServiceConfigQuery() 11 - const {data: profile} = useProfileQuery({did: currentAccount?.did}) 12 + const {data: profile} = useProfileQuery({ 13 + did: currentAccount?.did, 14 + staleTime: STALE.INFINITY, 15 + }) 12 16 13 17 const checkEmailConfirmed = !!serviceConfig?.checkEmailConfirmed 14 18
+159 -64
src/lib/notifications/notifications.ts
··· 1 - import React from 'react' 1 + import {useCallback, useEffect} from 'react' 2 + import {Platform} from 'react-native' 2 3 import * as Notifications from 'expo-notifications' 3 4 import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications' 4 - import {type BskyAgent} from '@atproto/api' 5 + import {type AtpAgent} from '@atproto/api' 6 + import debounce from 'lodash.debounce' 5 7 6 - import {logEvent} from '#/lib/statsig/statsig' 8 + import {PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID} from '#/lib/constants' 7 9 import {Logger} from '#/logger' 8 - import {devicePlatform, isAndroid, isNative} from '#/platform/detection' 10 + import {isNative} from '#/platform/detection' 9 11 import {type SessionAccount, useAgent, useSession} from '#/state/session' 10 - import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler' 11 - 12 - const SERVICE_DID = (serviceUrl?: string) => 13 - serviceUrl?.includes('staging') 14 - ? 'did:web:api.staging.bsky.dev' 15 - : 'did:web:api.bsky.app' 12 + import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' 16 13 17 14 const logger = Logger.create(Logger.Context.Notifications) 18 15 19 - async function registerPushToken( 20 - agent: BskyAgent, 21 - account: SessionAccount, 22 - token: Notifications.DevicePushToken, 23 - ) { 16 + /** 17 + * @private 18 + * Registers the device's push notification token with the Bluesky server. 19 + */ 20 + async function _registerPushToken({ 21 + agent, 22 + currentAccount, 23 + token, 24 + }: { 25 + agent: AtpAgent 26 + currentAccount: SessionAccount 27 + token: Notifications.DevicePushToken 28 + }) { 24 29 try { 25 - await agent.api.app.bsky.notification.registerPush({ 26 - serviceDid: SERVICE_DID(account.service), 27 - platform: devicePlatform, 30 + await agent.app.bsky.notification.registerPush({ 31 + serviceDid: currentAccount.service?.includes('staging') 32 + ? PUBLIC_STAGING_APPVIEW_DID 33 + : PUBLIC_APPVIEW_DID, 34 + platform: Platform.OS, 28 35 token: token.data, 29 36 appId: 'social.deer', 30 37 }) 31 - logger.debug('Notifications: Sent push token (init)', { 38 + 39 + logger.debug(`registerPushToken: success`, { 32 40 tokenType: token.type, 33 41 token: token.data, 34 42 }) 35 43 } catch (error) { 36 - logger.error('Notifications: Failed to set push token', {message: error}) 44 + logger.error(`registerPushToken: failed`, {safeMessage: error}) 37 45 } 38 46 } 39 47 40 - async function getPushToken(skipPermissionCheck = false) { 41 - const granted = 42 - skipPermissionCheck || (await Notifications.getPermissionsAsync()).granted 48 + /** 49 + * @private 50 + * Debounced version of `_registerPushToken` to prevent multiple calls. 51 + */ 52 + const _registerPushTokenDebounced = debounce(_registerPushToken, 100) 53 + 54 + /** 55 + * Hook to register the device's push notification token with the Bluesky. If 56 + * the user is not logged in, this will do nothing. 57 + * 58 + * Use this instead of using `_registerPushToken` or 59 + * `_registerPushTokenDebounced` directly. 60 + */ 61 + export function useRegisterPushToken() { 62 + const agent = useAgent() 63 + const {currentAccount} = useSession() 64 + 65 + return useCallback( 66 + ({token}: {token: Notifications.DevicePushToken}) => { 67 + if (!currentAccount) return 68 + return _registerPushTokenDebounced({ 69 + agent, 70 + currentAccount, 71 + token, 72 + }) 73 + }, 74 + [agent, currentAccount], 75 + ) 76 + } 77 + 78 + /** 79 + * Retreive the device's push notification token, if permissions are granted. 80 + */ 81 + async function getPushToken() { 82 + const granted = (await Notifications.getPermissionsAsync()).granted 83 + logger.debug(`getPushToken`, {granted}) 43 84 if (granted) { 44 85 return Notifications.getDevicePushTokenAsync() 45 86 } 46 87 } 47 88 89 + /** 90 + * Hook to get the device push token and register it with the Bluesky server. 91 + * Should only be called after a user has logged-in, since registration is an 92 + * authed endpoint. 93 + * 94 + * N.B. A previous regression in `expo-notifications` caused 95 + * `addPushTokenListener` to not fire on Android after calling 96 + * `getPushToken()`. Therefore, as insurance, we also call 97 + * `registerPushToken` here. 98 + * 99 + * Because `registerPushToken` is debounced, even if the the listener _does_ 100 + * fire, it's OK to also call `registerPushToken` below since only a single 101 + * call will be made to the server (ideally). This does race the listener (if 102 + * it fires), so there's a possibility that multiple calls will be made, but 103 + * that is acceptable. 104 + * 105 + * @see https://github.com/bluesky-social/social-app/pull/4467 106 + * @see https://github.com/expo/expo/issues/28656 107 + * @see https://github.com/expo/expo/issues/29909 108 + */ 109 + export function useGetAndRegisterPushToken() { 110 + const registerPushToken = useRegisterPushToken() 111 + return useCallback(async () => { 112 + /** 113 + * This will also fire the listener added via `addPushTokenListener`. That 114 + * listener also handles registration. 115 + */ 116 + const token = await getPushToken() 117 + 118 + logger.debug(`useGetAndRegisterPushToken`, {token: token ?? 'undefined'}) 119 + 120 + if (token) { 121 + /** 122 + * The listener should have registered the token already, but just in 123 + * case, call the debounced function again. 124 + */ 125 + registerPushToken({token}) 126 + } 127 + 128 + return token 129 + }, [registerPushToken]) 130 + } 131 + 132 + /** 133 + * Hook to register the device's push notification token with the Bluesky 134 + * server, as well as listen for push token updates, should they occurr. 135 + * 136 + * Registered via the shell, which wraps the navigation stack, meaning if we 137 + * have a current account, this handling will be registered and ready to go. 138 + */ 48 139 export function useNotificationsRegistration() { 49 - const agent = useAgent() 50 140 const {currentAccount} = useSession() 141 + const registerPushToken = useRegisterPushToken() 142 + const getAndRegisterPushToken = useGetAndRegisterPushToken() 51 143 52 - React.useEffect(() => { 53 - if (!currentAccount) { 54 - return 55 - } 144 + useEffect(() => { 145 + /** 146 + * We want this to init right away _after_ we have a logged in user. 147 + */ 148 + if (!currentAccount) return 56 149 57 - // HACK - see https://github.com/bluesky-social/social-app/pull/4467 58 - // An apparent regression in expo-notifications causes `addPushTokenListener` to not fire on Android whenever the 59 - // token changes by calling `getPushToken()`. This is a workaround to ensure we register the token once it is 60 - // generated on Android. 61 - if (isAndroid) { 62 - ;(async () => { 63 - const token = await getPushToken() 150 + logger.debug(`useNotificationsRegistration`) 64 151 65 - // Token will be undefined if we don't have notifications permission 66 - if (token) { 67 - registerPushToken(agent, currentAccount, token) 68 - } 69 - })() 70 - } else { 71 - getPushToken() 72 - } 152 + /** 153 + * Init push token, if permissions are granted already. If they weren't, 154 + * they'll be requested by the `useRequestNotificationsPermission` hook 155 + * below. 156 + */ 157 + getAndRegisterPushToken() 73 158 74 - // According to the Expo docs, there is a chance that the token will change while the app is open in some rare 75 - // cases. This will fire `registerPushToken` whenever that happens. 76 - const subscription = Notifications.addPushTokenListener(async newToken => { 77 - registerPushToken(agent, currentAccount, newToken) 159 + /** 160 + * Register the push token with the Bluesky server, whenever it changes. 161 + * This is also fired any time `getDevicePushTokenAsync` is called. 162 + * 163 + * According to the Expo docs, there is a chance that the token will change 164 + * while the app is open in some rare cases. This will fire 165 + * `registerPushToken` whenever that happens. 166 + * 167 + * @see https://docs.expo.dev/versions/latest/sdk/notifications/#addpushtokenlistenerlistener 168 + */ 169 + const subscription = Notifications.addPushTokenListener(async token => { 170 + registerPushToken({token}) 171 + logger.debug(`addPushTokenListener callback`, {token}) 78 172 }) 79 173 80 174 return () => { 81 175 subscription.remove() 82 176 } 83 - }, [currentAccount, agent]) 177 + }, [currentAccount, getAndRegisterPushToken, registerPushToken]) 84 178 } 85 179 86 180 export function useRequestNotificationsPermission() { 87 181 const {currentAccount} = useSession() 88 - const agent = useAgent() 182 + const getAndRegisterPushToken = useGetAndRegisterPushToken() 89 183 90 184 return async ( 91 185 context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home', ··· 107 201 } 108 202 109 203 const res = await Notifications.requestPermissionsAsync() 110 - logEvent('notifications:request', { 204 + 205 + logger.metric(`notifications:request`, { 111 206 context: context, 112 207 status: res.status, 113 208 }) 114 209 115 210 if (res.granted) { 116 - // This will fire a pushTokenEvent, which will handle registration of the token 117 - const token = await getPushToken(true) 118 - 119 - // Same hack as above. We cannot rely on the `addPushTokenListener` to fire on Android due to an Expo bug, so we 120 - // will manually register it here. Note that this will occur only: 121 - // 1. right after the user signs in, leading to no `currentAccount` account being available - this will be instead 122 - // picked up from the useEffect above on `currentAccount` change 123 - // 2. right after onboarding. In this case, we _need_ this registration, since `currentAccount` will not change 124 - // and we need to ensure the token is registered right after permission is granted. `currentAccount` will already 125 - // be available in this case, so the registration will succeed. 126 - // We should remove this once expo-notifications (and possibly FCMv1) is fixed and the `addPushTokenListener` is 127 - // working again. See https://github.com/expo/expo/issues/28656 128 - if (isAndroid && currentAccount && token) { 129 - registerPushToken(agent, currentAccount, token) 211 + if (currentAccount) { 212 + /** 213 + * If we have an account in scope, we can safely call 214 + * `getAndRegisterPushToken`. 215 + */ 216 + getAndRegisterPushToken() 217 + } else { 218 + /** 219 + * Right after login, `currentAccount` in this scope will be undefined, 220 + * but calling `getPushToken` will result in `addPushTokenListener` 221 + * listeners being called, which will handle the registration with the 222 + * Bluesky server. 223 + */ 224 + getPushToken() 130 225 } 131 226 } 132 227 }
+24 -22
src/locale/locales/en/messages.po
··· 456 456 msgid "<0>{0}</0> is included in your starter pack" 457 457 msgstr "" 458 458 459 - #: src/components/WhoCanReply.tsx:302 459 + #: src/components/WhoCanReply.tsx:317 460 460 msgid "<0>{0}</0> members" 461 461 msgstr "" 462 462 ··· 939 939 msgid "an unknown labeler" 940 940 msgstr "" 941 941 942 - #: src/components/WhoCanReply.tsx:323 942 + #: src/components/WhoCanReply.tsx:338 943 943 msgid "and" 944 944 msgstr "" 945 945 ··· 1276 1276 msgid "Blocked post." 1277 1277 msgstr "" 1278 1278 1279 - #: src/screens/Profile/Sections/Labels.tsx:170 1279 + #: src/screens/Profile/Sections/Labels.tsx:203 1280 1280 msgid "Blocking does not prevent this labeler from placing labels on your account." 1281 1281 msgstr "" 1282 1282 ··· 1741 1741 #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:123 1742 1742 #: src/components/verification/VerificationsDialog.tsx:144 1743 1743 #: src/components/verification/VerifierDialog.tsx:144 1744 + #: src/components/WhoCanReply.tsx:200 1745 + #: src/components/WhoCanReply.tsx:207 1744 1746 #: src/view/com/modals/ChangePassword.tsx:279 1745 1747 #: src/view/com/modals/ChangePassword.tsx:282 1746 1748 msgid "Close" ··· 2433 2435 msgid "Developer options" 2434 2436 msgstr "" 2435 2437 2436 - #: src/components/WhoCanReply.tsx:185 2438 + #: src/components/WhoCanReply.tsx:186 2437 2439 msgid "Dialog: adjust who can interact with this post" 2438 2440 msgstr "" 2439 2441 ··· 2988 2990 msgid "Everybody can reply" 2989 2991 msgstr "" 2990 2992 2991 - #: src/components/WhoCanReply.tsx:228 2993 + #: src/components/WhoCanReply.tsx:243 2992 2994 msgid "Everybody can reply to this post." 2993 2995 msgstr "" 2994 2996 ··· 3058 3060 msgid "Expands or collapses post text" 3059 3061 msgstr "" 3060 3062 3061 - #: src/lib/api/index.ts:406 3063 + #: src/lib/api/index.ts:418 3062 3064 msgid "Expected uri to resolve to a record" 3063 3065 msgstr "" 3064 3066 ··· 4250 4252 msgid "Labels added" 4251 4253 msgstr "" 4252 4254 4253 - #: src/screens/Profile/Sections/Labels.tsx:160 4255 + #: src/screens/Profile/Sections/Labels.tsx:194 4254 4256 msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network." 4255 4257 msgstr "" 4256 4258 ··· 4356 4358 msgstr "" 4357 4359 4358 4360 #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:84 4359 - msgid "Leave them all unchecked to see any language." 4361 + msgid "Leave them all unselected to see any language." 4360 4362 msgstr "" 4361 4363 4362 4364 #: src/components/dialogs/LinkWarning.tsx:67 ··· 4680 4682 msgid "Mention notifications" 4681 4683 msgstr "" 4682 4684 4683 - #: src/components/WhoCanReply.tsx:269 4685 + #: src/components/WhoCanReply.tsx:284 4684 4686 msgid "mentioned users" 4685 4687 msgstr "" 4686 4688 ··· 5193 5195 msgid "No one" 5194 5196 msgstr "" 5195 5197 5196 - #: src/components/WhoCanReply.tsx:252 5198 + #: src/components/WhoCanReply.tsx:267 5197 5199 msgid "No one but the author can quote this post." 5198 5200 msgstr "" 5199 5201 ··· 5419 5421 msgid "Only .jpg and .png files are supported" 5420 5422 msgstr "" 5421 5423 5422 - #: src/components/WhoCanReply.tsx:232 5424 + #: src/components/WhoCanReply.tsx:247 5423 5425 msgid "Only {0} can reply." 5424 5426 msgstr "" 5425 5427 ··· 6521 6523 msgid "Replies disabled" 6522 6524 msgstr "" 6523 6525 6524 - #: src/components/WhoCanReply.tsx:230 6526 + #: src/components/WhoCanReply.tsx:245 6525 6527 msgid "Replies to this post are disabled." 6526 6528 msgstr "" 6527 6529 ··· 7674 7676 7675 7677 #: src/components/ReportDialog/index.tsx:54 7676 7678 #: src/screens/Moderation/index.tsx:101 7677 - #: src/screens/Profile/Sections/Labels.tsx:121 7679 + #: src/screens/Profile/Sections/Labels.tsx:184 7678 7680 msgid "Something went wrong, please try again." 7679 7681 msgstr "" 7680 7682 ··· 7694 7696 msgid "Something wrong? Let us know." 7695 7697 msgstr "" 7696 7698 7697 - #: src/App.native.tsx:122 7699 + #: src/App.native.tsx:123 7698 7700 #: src/App.web.tsx:99 7699 7701 msgid "Sorry! Your session expired. Please sign in again." 7700 7702 msgstr "" ··· 7834 7836 msgid "Subscribe" 7835 7837 msgstr "" 7836 7838 7837 - #: src/screens/Profile/Sections/Labels.tsx:198 7839 + #: src/screens/Profile/Sections/Labels.tsx:231 7838 7840 msgid "Subscribe to @{0} to use these labels:" 7839 7841 msgstr "" 7840 7842 ··· 8358 8360 msgid "This label was applied by you." 8359 8361 msgstr "" 8360 8362 8361 - #: src/screens/Profile/Sections/Labels.tsx:185 8363 + #: src/screens/Profile/Sections/Labels.tsx:218 8362 8364 msgid "This labeler hasn't declared what labels it publishes, and may not be active." 8363 8365 msgstr "" 8364 8366 ··· 8387 8389 msgid "This post claims to have been created on <0>{0}</0>, but was first seen by Bluesky on <1>{1}</1>." 8388 8390 msgstr "" 8389 8391 8390 - #: src/components/WhoCanReply.tsx:223 8392 + #: src/components/WhoCanReply.tsx:238 8391 8393 msgid "This post has an unknown type of threadgate on it. Your app may be out of date." 8392 8394 msgstr "" 8393 8395 ··· 8888 8890 msgid "Uploading images..." 8889 8891 msgstr "" 8890 8892 8891 - #: src/lib/api/index.ts:356 8892 - #: src/lib/api/index.ts:380 8893 + #: src/lib/api/index.ts:368 8894 + #: src/lib/api/index.ts:392 8893 8895 msgid "Uploading link thumbnail..." 8894 8896 msgstr "" 8895 8897 ··· 8984 8986 msgid "Username or email address" 8985 8987 msgstr "" 8986 8988 8987 - #: src/components/WhoCanReply.tsx:286 8989 + #: src/components/WhoCanReply.tsx:301 8988 8990 msgid "users followed by <0>@{0}</0>" 8989 8991 msgstr "" 8990 8992 8991 - #: src/components/WhoCanReply.tsx:273 8993 + #: src/components/WhoCanReply.tsx:288 8992 8994 msgid "users following <0>@{0}</0>" 8993 8995 msgstr "" 8994 8996 ··· 9416 9418 msgid "Which languages would you like to see in your algorithmic feeds?" 9417 9419 msgstr "" 9418 9420 9419 - #: src/components/WhoCanReply.tsx:189 9421 + #: src/components/WhoCanReply.tsx:190 9420 9422 msgid "Who can interact with this post?" 9421 9423 msgstr "" 9422 9424
-1
src/platform/detection.ts
··· 3 3 export const isIOS = Platform.OS === 'ios' 4 4 export const isAndroid = Platform.OS === 'android' 5 5 export const isNative = isIOS || isAndroid 6 - export const devicePlatform = isIOS ? 'ios' : isAndroid ? 'android' : 'web' 7 6 export const isWeb = !isNative 8 7 export const isMobileWebMediaQuery = 'only screen and (max-width: 1300px)' 9 8 export const isMobileWeb =
+15 -4
src/screens/PostThread/index.tsx
··· 45 45 export function PostThread({uri}: {uri: string}) { 46 46 const {gtMobile} = useBreakpoints() 47 47 const {hasSession} = useSession() 48 - const initialNumToRender = useInitialNumToRender() // TODO 48 + const initialNumToRender = useInitialNumToRender() 49 49 const {height: windowHeight} = useWindowDimensions() 50 50 const anchorPostSource = useUnstablePostSource(uri) 51 51 const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession) ··· 522 522 })} 523 523 onStartReached={onStartReached} 524 524 onEndReached={onEndReached} 525 - onEndReachedThreshold={2} 525 + onEndReachedThreshold={4} 526 526 onStartReachedThreshold={1} 527 527 /** 528 528 * NATIVE ONLY ··· 530 530 */ 531 531 maintainVisibleContentPosition={{minIndexForVisible: 0}} 532 532 desktopFixedHeight 533 + sideBorders={false} 533 534 ListFooterComponent={ 534 535 <ListFooter 535 536 /* ··· 551 552 /> 552 553 } 553 554 initialNumToRender={initialNumToRender} 554 - windowSize={11} 555 - sideBorders={false} 555 + /** 556 + * Default: 21 557 + */ 558 + windowSize={7} 559 + /** 560 + * Default: 10 561 + */ 562 + maxToRenderPerBatch={5} 563 + /** 564 + * Default: 50 565 + */ 566 + updateCellsBatchingPeriod={100} 556 567 /> 557 568 )} 558 569
+171 -162
src/screens/Profile/Sections/Labels.tsx
··· 1 - import React from 'react' 2 - import {findNodeHandle, View} from 'react-native' 3 - import type Animated from 'react-native-reanimated' 4 - import {useSafeAreaFrame} from 'react-native-safe-area-context' 1 + import {useCallback, useEffect, useImperativeHandle, useMemo} from 'react' 2 + import {findNodeHandle, type ListRenderItemInfo, View} from 'react-native' 5 3 import { 6 4 type AppBskyLabelerDefs, 7 5 type InterpretedLabelValueDefinition, ··· 11 9 import {msg, Trans} from '@lingui/macro' 12 10 import {useLingui} from '@lingui/react' 13 11 14 - import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 15 12 import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation' 16 - import {useScrollHandlers} from '#/lib/ScrollContext' 17 13 import {isIOS, isNative} from '#/platform/detection' 18 - import {type ListRef} from '#/view/com/util/List' 19 - import {atoms as a, useTheme} from '#/alf' 14 + import {List, type ListRef} from '#/view/com/util/List' 15 + import {atoms as a, ios, tokens, useTheme} from '#/alf' 20 16 import {Divider} from '#/components/Divider' 21 17 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 22 - import * as Layout from '#/components/Layout' 18 + import {ListFooter} from '#/components/Lists' 23 19 import {Loader} from '#/components/Loader' 24 20 import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' 25 21 import {Text} from '#/components/Typography' ··· 27 23 import {type SectionRef} from './types' 28 24 29 25 interface LabelsSectionProps { 26 + ref: React.Ref<SectionRef> 30 27 isLabelerLoading: boolean 31 28 labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed | undefined 32 29 labelerError: Error | null ··· 36 33 isFocused: boolean 37 34 setScrollViewTag: (tag: number | null) => void 38 35 } 39 - export const ProfileLabelsSection = React.forwardRef< 40 - SectionRef, 41 - LabelsSectionProps 42 - >(function LabelsSectionImpl( 43 - { 44 - isLabelerLoading, 45 - labelerInfo, 46 - labelerError, 47 - moderationOpts, 48 - scrollElRef, 49 - headerHeight, 50 - isFocused, 51 - setScrollViewTag, 52 - }, 36 + export function ProfileLabelsSection({ 53 37 ref, 54 - ) { 55 - const {_} = useLingui() 56 - const {height: minHeight} = useSafeAreaFrame() 57 - 58 - // Intentionally destructured outside the main thread closure. 59 - // See https://github.com/bluesky-social/social-app/pull/4108. 60 - const { 61 - onBeginDrag: onBeginDragFromContext, 62 - onEndDrag: onEndDragFromContext, 63 - onScroll: onScrollFromContext, 64 - onMomentumEnd: onMomentumEndFromContext, 65 - } = useScrollHandlers() 66 - const scrollHandler = useAnimatedScrollHandler({ 67 - onBeginDrag(e, ctx) { 68 - onBeginDragFromContext?.(e, ctx) 69 - }, 70 - onEndDrag(e, ctx) { 71 - onEndDragFromContext?.(e, ctx) 72 - }, 73 - onScroll(e, ctx) { 74 - onScrollFromContext?.(e, ctx) 75 - }, 76 - onMomentumEnd(e, ctx) { 77 - onMomentumEndFromContext?.(e, ctx) 78 - }, 79 - }) 38 + isLabelerLoading, 39 + labelerInfo, 40 + labelerError, 41 + moderationOpts, 42 + scrollElRef, 43 + headerHeight, 44 + isFocused, 45 + setScrollViewTag, 46 + }: LabelsSectionProps) { 47 + const t = useTheme() 80 48 81 - const onScrollToTop = React.useCallback(() => { 82 - // @ts-ignore TODO fix this 49 + const onScrollToTop = useCallback(() => { 50 + // @ts-expect-error TODO fix this 83 51 scrollElRef.current?.scrollTo({ 84 52 animated: isNative, 85 53 x: 0, ··· 87 55 }) 88 56 }, [scrollElRef, headerHeight]) 89 57 90 - React.useImperativeHandle(ref, () => ({ 58 + useImperativeHandle(ref, () => ({ 91 59 scrollToTop: onScrollToTop, 92 60 })) 93 61 94 - React.useEffect(() => { 62 + useEffect(() => { 95 63 if (isIOS && isFocused && scrollElRef.current) { 96 64 const nativeTag = findNodeHandle(scrollElRef.current) 97 65 setScrollViewTag(nativeTag) 98 66 } 99 67 }, [isFocused, scrollElRef, setScrollViewTag]) 100 68 69 + const isSubscribed = labelerInfo 70 + ? !!isLabelerSubscribed(labelerInfo, moderationOpts) 71 + : false 72 + 73 + const labelValues = useMemo(() => { 74 + if (isLabelerLoading || !labelerInfo || labelerError) return [] 75 + const customDefs = interpretLabelValueDefinitions(labelerInfo) 76 + return labelerInfo.policies.labelValues 77 + .filter((val, i, arr) => arr.indexOf(val) === i) // dedupe 78 + .map(val => lookupLabelValueDefinition(val, customDefs)) 79 + .filter( 80 + def => def && def?.configurable, 81 + ) as InterpretedLabelValueDefinition[] 82 + }, [labelerInfo, labelerError, isLabelerLoading]) 83 + 84 + const numItems = labelValues.length 85 + 86 + const renderItem = useCallback( 87 + ({item, index}: ListRenderItemInfo<InterpretedLabelValueDefinition>) => { 88 + if (!labelerInfo) return null 89 + return ( 90 + <View 91 + style={[ 92 + t.atoms.bg_contrast_25, 93 + index === 0 && [ 94 + a.overflow_hidden, 95 + { 96 + borderTopLeftRadius: tokens.borderRadius.md, 97 + borderTopRightRadius: tokens.borderRadius.md, 98 + }, 99 + ], 100 + index === numItems - 1 && [ 101 + a.overflow_hidden, 102 + { 103 + borderBottomLeftRadius: tokens.borderRadius.md, 104 + borderBottomRightRadius: tokens.borderRadius.md, 105 + }, 106 + ], 107 + ]}> 108 + {index !== 0 && <Divider />} 109 + <LabelerLabelPreference 110 + disabled={isSubscribed ? undefined : true} 111 + labelDefinition={item} 112 + labelerDid={labelerInfo.creator.did} 113 + /> 114 + </View> 115 + ) 116 + }, 117 + [labelerInfo, isSubscribed, numItems, t], 118 + ) 119 + 101 120 return ( 102 - <Layout.Center style={{minHeight}}> 103 - <Layout.Content 104 - ref={scrollElRef as React.Ref<Animated.ScrollView>} 105 - scrollEventThrottle={1} 106 - contentContainerStyle={{ 107 - paddingTop: headerHeight, 108 - borderWidth: 0, 109 - }} 110 - contentOffset={{x: 0, y: headerHeight * -1}} 111 - onScroll={scrollHandler}> 112 - {isLabelerLoading ? ( 113 - <View style={[a.w_full, a.align_center, a.py_4xl]}> 114 - <Loader size="xl" /> 115 - </View> 116 - ) : labelerError || !labelerInfo ? ( 117 - <View style={[a.w_full, a.align_center, a.py_4xl]}> 118 - <ErrorState 119 - error={ 120 - labelerError?.toString() || 121 - _(msg`Something went wrong, please try again.`) 122 - } 123 - /> 124 - </View> 125 - ) : ( 126 - <ProfileLabelsSectionInner 127 - moderationOpts={moderationOpts} 121 + <View> 122 + <List 123 + ref={scrollElRef} 124 + data={labelValues} 125 + renderItem={renderItem} 126 + keyExtractor={keyExtractor} 127 + contentContainerStyle={a.px_xl} 128 + headerOffset={headerHeight} 129 + progressViewOffset={ios(0)} 130 + ListHeaderComponent={ 131 + <LabelerListHeader 132 + isLabelerLoading={isLabelerLoading} 128 133 labelerInfo={labelerInfo} 134 + labelerError={labelerError} 135 + hasValues={labelValues.length !== 0} 136 + isSubscribed={isSubscribed} 129 137 /> 130 - )} 131 - </Layout.Content> 132 - </Layout.Center> 138 + } 139 + ListFooterComponent={ 140 + <ListFooter 141 + height={headerHeight + 180} 142 + style={a.border_transparent} 143 + /> 144 + } 145 + /> 146 + </View> 133 147 ) 134 - }) 148 + } 135 149 136 - export function ProfileLabelsSectionInner({ 137 - moderationOpts, 150 + function keyExtractor(item: InterpretedLabelValueDefinition) { 151 + return item.identifier 152 + } 153 + 154 + export function LabelerListHeader({ 155 + isLabelerLoading, 156 + labelerError, 138 157 labelerInfo, 158 + hasValues, 159 + isSubscribed, 139 160 }: { 140 - moderationOpts: ModerationOpts 141 - labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed 161 + isLabelerLoading: boolean 162 + labelerError?: Error | null 163 + labelerInfo?: AppBskyLabelerDefs.LabelerViewDetailed 164 + hasValues: boolean 165 + isSubscribed: boolean 142 166 }) { 143 167 const t = useTheme() 168 + const {_} = useLingui() 144 169 145 - const {labelValues} = labelerInfo.policies 146 - const isSubscribed = isLabelerSubscribed(labelerInfo, moderationOpts) 147 - const labelDefs = React.useMemo(() => { 148 - const customDefs = interpretLabelValueDefinitions(labelerInfo) 149 - return labelValues 150 - .map(val => lookupLabelValueDefinition(val, customDefs)) 151 - .filter( 152 - def => def && def?.configurable, 153 - ) as InterpretedLabelValueDefinition[] 154 - }, [labelerInfo, labelValues]) 170 + if (isLabelerLoading) { 171 + return ( 172 + <View style={[a.w_full, a.align_center, a.py_4xl]}> 173 + <Loader size="xl" /> 174 + </View> 175 + ) 176 + } 177 + 178 + if (labelerError || !labelerInfo) { 179 + return ( 180 + <View style={[a.w_full, a.align_center, a.py_4xl]}> 181 + <ErrorState 182 + error={ 183 + labelerError?.toString() || 184 + _(msg`Something went wrong, please try again.`) 185 + } 186 + /> 187 + </View> 188 + ) 189 + } 155 190 156 191 return ( 157 - <View style={[a.pt_xl, a.px_lg, a.border_t, t.atoms.border_contrast_low]}> 158 - <View> 159 - <Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}> 160 - <Trans> 161 - Labels are annotations on users and content. They can be used to 162 - hide, warn, and categorize the network. 163 - </Trans> 164 - </Text> 165 - {labelerInfo.creator.viewer?.blocking ? ( 166 - <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}> 167 - <CircleInfo size="sm" fill={t.atoms.text_contrast_medium.color} /> 168 - <Text 169 - style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}> 170 - <Trans> 171 - Blocking does not prevent this labeler from placing labels on 172 - your account. 173 - </Trans> 174 - </Text> 175 - </View> 176 - ) : null} 177 - {labelValues.length === 0 ? ( 178 - <Text 179 - style={[ 180 - a.pt_xl, 181 - t.atoms.text_contrast_high, 182 - a.leading_snug, 183 - a.text_sm, 184 - ]}> 192 + <View style={[a.py_xl]}> 193 + <Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}> 194 + <Trans> 195 + Labels are annotations on users and content. They can be used to hide, 196 + warn, and categorize the network. 197 + </Trans> 198 + </Text> 199 + {labelerInfo?.creator.viewer?.blocking ? ( 200 + <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}> 201 + <CircleInfo size="sm" fill={t.atoms.text_contrast_medium.color} /> 202 + <Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}> 185 203 <Trans> 186 - This labeler hasn't declared what labels it publishes, and may not 187 - be active. 204 + Blocking does not prevent this labeler from placing labels on your 205 + account. 188 206 </Trans> 189 207 </Text> 190 - ) : !isSubscribed ? ( 191 - <Text 192 - style={[ 193 - a.pt_xl, 194 - t.atoms.text_contrast_high, 195 - a.leading_snug, 196 - a.text_sm, 197 - ]}> 198 - <Trans> 199 - Subscribe to @{labelerInfo.creator.handle} to use these labels: 200 - </Trans> 201 - </Text> 202 - ) : null} 203 - </View> 204 - {labelDefs.length > 0 && ( 205 - <View 208 + </View> 209 + ) : null} 210 + {!hasValues ? ( 211 + <Text 212 + style={[ 213 + a.pt_xl, 214 + t.atoms.text_contrast_high, 215 + a.leading_snug, 216 + a.text_sm, 217 + ]}> 218 + <Trans> 219 + This labeler hasn't declared what labels it publishes, and may not 220 + be active. 221 + </Trans> 222 + </Text> 223 + ) : !isSubscribed ? ( 224 + <Text 206 225 style={[ 207 - a.mt_xl, 208 - a.w_full, 209 - a.rounded_md, 210 - a.overflow_hidden, 211 - t.atoms.bg_contrast_25, 226 + a.pt_xl, 227 + t.atoms.text_contrast_high, 228 + a.leading_snug, 229 + a.text_sm, 212 230 ]}> 213 - {labelDefs.map((labelDef, i) => { 214 - return ( 215 - <React.Fragment key={labelDef.identifier}> 216 - {i !== 0 && <Divider />} 217 - <LabelerLabelPreference 218 - disabled={isSubscribed ? undefined : true} 219 - labelDefinition={labelDef} 220 - labelerDid={labelerInfo.creator.did} 221 - /> 222 - </React.Fragment> 223 - ) 224 - })} 225 - </View> 226 - )} 231 + <Trans> 232 + Subscribe to @{labelerInfo.creator.handle} to use these labels: 233 + </Trans> 234 + </Text> 235 + ) : null} 227 236 </View> 228 237 ) 229 238 }
+2 -2
src/state/queries/labeler.ts
··· 1 - import {AppBskyLabelerDefs} from '@atproto/api' 1 + import {type AppBskyLabelerDefs} from '@atproto/api' 2 2 import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' 3 3 import {z} from 'zod' 4 4 ··· 41 41 queryKey: labelerInfoQueryKey(did as string), 42 42 queryFn: async () => { 43 43 const res = await agent.app.bsky.labeler.getServices({ 44 - dids: [did as string], 44 + dids: [did!], 45 45 detailed: true, 46 46 }) 47 47 return res.data.views[0] as AppBskyLabelerDefs.LabelerViewDetailed
+1 -1
src/view/com/composer/videos/pickVideo.ts
··· 1 1 import { 2 - ImagePickerAsset, 2 + type ImagePickerAsset, 3 3 launchImageLibraryAsync, 4 4 UIImagePickerPreferredAssetRepresentationMode, 5 5 } from 'expo-image-picker'
+1 -1
src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
··· 81 81 </Trans> 82 82 </Text> 83 83 <Text style={[pal.textLight, styles.description]}> 84 - <Trans>Leave them all unchecked to see any language.</Trans> 84 + <Trans>Leave them all unselected to see any language.</Trans> 85 85 </Text> 86 86 <ScrollView style={styles.scrollContainer}> 87 87 {languages.map(lang => (
+4 -4
yarn.lock
··· 16855 16855 resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz#4bba0f5f04e2cf222494cce3a9794ba6a4894dee" 16856 16856 integrity sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg== 16857 16857 16858 - react-native-pager-view@^6.7.1: 16859 - version "6.8.1" 16860 - resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.8.1.tgz#fa0ec09ea7c44190c7c013d75dd09fdc17b96100" 16861 - integrity sha512-XIyVEMhwq7sZqM7GobOJZXxFCfdFgVNq/CFB2rZIRNRSVPJqE1k1fsc8xfQKfdzsp6Rpt6I7VOIvhmP7/YHdVg== 16858 + react-native-pager-view@6.8.0: 16859 + version "6.8.0" 16860 + resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.8.0.tgz#5bac05203d911bf9bf039d47db41b1313dbd1a7a" 16861 + integrity sha512-/wgFV8nB4TLnQ6j9e3TvNrHbPF5TINMZHaXt86GOV0NSJNMVGkWguniJVKrYLm85LL8KVhRkgdh43Rdu7PvW1A== 16862 16862 16863 16863 react-native-progress@bluesky-social/react-native-progress: 16864 16864 version "5.0.0"