Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

Fix problems with `BottomSheet` and the report dialog (#3297)

* use @discord/bottom-sheet

* add @types/invariant

* some progress on keyboard dialog

* rework

rework

add a comment

use discord bottom sheet

* remove `@gorhom/bottom-sheet`

* remove android specific code

* organize imports

authored by hailey.at and committed by

GitHub ad3dd9f6 c649ee1a

+128 -116
+2 -1
package.json
··· 49 49 "@atproto/api": "^0.12.2", 50 50 "@bam.tech/react-native-image-resizer": "^3.0.4", 51 51 "@braintree/sanitize-url": "^6.0.2", 52 + "@discord/bottom-sheet": "https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1", 52 53 "@emoji-mart/react": "^1.1.1", 53 54 "@expo/html-elements": "^0.4.2", 54 55 "@expo/webpack-config": "^19.0.0", ··· 56 57 "@fortawesome/free-regular-svg-icons": "^6.1.1", 57 58 "@fortawesome/free-solid-svg-icons": "^6.1.1", 58 59 "@fortawesome/react-native-fontawesome": "^0.3.0", 59 - "@gorhom/bottom-sheet": "^4.5.1", 60 60 "@lingui/react": "^4.5.0", 61 61 "@mattermost/react-native-paste-input": "^0.6.4", 62 62 "@miblanchard/react-native-slider": "^2.3.1", ··· 93 93 "@tiptap/pm": "^2.0.0-beta.220", 94 94 "@tiptap/react": "^2.0.0-beta.220", 95 95 "@tiptap/suggestion": "^2.0.0-beta.220", 96 + "@types/invariant": "^2.2.37", 96 97 "@types/node": "^18.16.2", 97 98 "@zxing/text-encoding": "^0.9.0", 98 99 "array.prototype.findlast": "^1.2.3",
+31 -32
src/App.native.tsx
··· 1 1 import 'react-native-url-polyfill/auto' 2 2 import 'lib/sentry' // must be near top 3 + import 'view/icons' 3 4 4 - import React, {useState, useEffect} from 'react' 5 - import {RootSiblingParent} from 'react-native-root-siblings' 6 - import * as SplashScreen from 'expo-splash-screen' 5 + import React, {useEffect, useState} from 'react' 7 6 import {GestureHandlerRootView} from 'react-native-gesture-handler' 8 - import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client' 7 + import {RootSiblingParent} from 'react-native-root-siblings' 9 8 import { 10 - SafeAreaProvider, 11 9 initialWindowMetrics, 10 + SafeAreaProvider, 12 11 } from 'react-native-safe-area-context' 13 - 14 - import 'view/icons' 12 + import * as SplashScreen from 'expo-splash-screen' 13 + import {StatusBar} from 'expo-status-bar' 14 + import {msg} from '@lingui/macro' 15 + import {useLingui} from '@lingui/react' 16 + import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client' 15 17 16 - import {ThemeProvider as Alf} from '#/alf' 17 - import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 18 + import {Provider as StatsigProvider} from '#/lib/statsig/statsig' 18 19 import {init as initPersistedState} from '#/state/persisted' 19 - import {listenSessionDropped} from './state/events' 20 - import {ThemeProvider} from 'lib/ThemeContext' 21 - import {s} from 'lib/styles' 22 - import {Shell} from 'view/shell' 20 + import * as persisted from '#/state/persisted' 21 + import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 22 + import {useIntentHandler} from 'lib/hooks/useIntentHandler' 23 23 import * as notifications from 'lib/notifications/notifications' 24 - import * as Toast from 'view/com/util/Toast' 25 24 import { 26 - queryClient, 27 25 asyncStoragePersister, 28 26 dehydrateOptions, 27 + queryClient, 29 28 } from 'lib/react-query' 30 - import {TestCtrls} from 'view/com/testing/TestCtrls' 31 - import {Provider as ShellStateProvider} from 'state/shell' 32 - import {Provider as ModalStateProvider} from 'state/modals' 29 + import {s} from 'lib/styles' 30 + import {ThemeProvider} from 'lib/ThemeContext' 31 + import {isAndroid} from 'platform/detection' 33 32 import {Provider as DialogStateProvider} from 'state/dialogs' 33 + import {Provider as InvitesStateProvider} from 'state/invites' 34 34 import {Provider as LightboxStateProvider} from 'state/lightbox' 35 + import {Provider as ModalStateProvider} from 'state/modals' 35 36 import {Provider as MutedThreadsProvider} from 'state/muted-threads' 36 - import {Provider as InvitesStateProvider} from 'state/invites' 37 37 import {Provider as PrefsStateProvider} from 'state/preferences' 38 - import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' 39 - import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed' 40 - import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 41 - import I18nProvider from './locale/i18nProvider' 38 + import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' 42 39 import { 43 40 Provider as SessionProvider, 44 41 useSession, 45 42 useSessionApi, 46 43 } from 'state/session' 47 - import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' 48 - import * as persisted from '#/state/persisted' 44 + import {Provider as ShellStateProvider} from 'state/shell' 45 + import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' 46 + import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed' 47 + import {TestCtrls} from 'view/com/testing/TestCtrls' 48 + import * as Toast from 'view/com/util/Toast' 49 + import {Shell} from 'view/shell' 50 + import {ThemeProvider as Alf} from '#/alf' 51 + import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 52 + import {Provider as PortalProvider} from '#/components/Portal' 49 53 import {Splash} from '#/Splash' 50 - import {Provider as PortalProvider} from '#/components/Portal' 51 - import {Provider as StatsigProvider} from '#/lib/statsig/statsig' 52 - import {msg} from '@lingui/macro' 53 - import {useLingui} from '@lingui/react' 54 - import {useIntentHandler} from 'lib/hooks/useIntentHandler' 55 - import {StatusBar} from 'expo-status-bar' 56 - import {isAndroid} from 'platform/detection' 54 + import I18nProvider from './locale/i18nProvider' 55 + import {listenSessionDropped} from './state/events' 57 56 58 57 SplashScreen.preventAutoHideAsync() 59 58
+20 -28
src/components/Dialog/index.tsx
··· 1 1 import React, {useImperativeHandle} from 'react' 2 - import {View, Dimensions, Keyboard, Pressable} from 'react-native' 2 + import {Dimensions, Pressable, View} from 'react-native' 3 + import Animated, {useAnimatedStyle} from 'react-native-reanimated' 4 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 3 5 import BottomSheet, { 4 6 BottomSheetBackdropProps, 5 7 BottomSheetScrollView, 8 + BottomSheetScrollViewMethods, 6 9 BottomSheetTextInput, 7 10 BottomSheetView, 8 11 useBottomSheet, 9 12 WINDOW_HEIGHT, 10 - } from '@gorhom/bottom-sheet' 11 - import {useSafeAreaInsets} from 'react-native-safe-area-context' 12 - import Animated, {useAnimatedStyle} from 'react-native-reanimated' 13 + } from '@discord/bottom-sheet/src' 13 14 14 - import {useTheme, atoms as a, flatten} from '#/alf' 15 - import {Portal} from '#/components/Portal' 16 - import {createInput} from '#/components/forms/TextField' 17 15 import {logger} from '#/logger' 18 16 import {useDialogStateControlContext} from '#/state/dialogs' 19 - 17 + import {isNative} from 'platform/detection' 18 + import {atoms as a, flatten, useTheme} from '#/alf' 19 + import {Context} from '#/components/Dialog/context' 20 20 import { 21 - DialogOuterProps, 22 21 DialogControlProps, 23 22 DialogInnerProps, 23 + DialogOuterProps, 24 24 } from '#/components/Dialog/types' 25 - import {Context} from '#/components/Dialog/context' 26 - import {isNative} from 'platform/detection' 25 + import {createInput} from '#/components/forms/TextField' 26 + import {Portal} from '#/components/Portal' 27 27 28 - export {useDialogControl, useDialogContext} from '#/components/Dialog/context' 28 + export {useDialogContext, useDialogControl} from '#/components/Dialog/context' 29 29 export * from '#/components/Dialog/types' 30 30 // @ts-ignore 31 31 export const Input = createInput(BottomSheetTextInput) ··· 122 122 ) 123 123 124 124 const onCloseInner = React.useCallback(() => { 125 - Keyboard.dismiss() 126 125 try { 127 126 closeCallback.current?.() 128 127 } catch (e: any) { ··· 206 205 ) 207 206 } 208 207 209 - export function ScrollableInner({ 210 - children, 211 - keyboardDismissMode, 212 - style, 213 - }: DialogInnerProps) { 208 + export const ScrollableInner = React.forwardRef< 209 + BottomSheetScrollViewMethods, 210 + DialogInnerProps 211 + >(function ScrollableInner({children, style}, ref) { 214 212 const insets = useSafeAreaInsets() 215 213 return ( 216 214 <BottomSheetScrollView 217 215 keyboardShouldPersistTaps="handled" 218 - keyboardDismissMode={keyboardDismissMode || 'on-drag'} 219 216 style={[ 220 217 a.flex_1, // main diff is this 221 218 a.p_xl, ··· 227 224 }, 228 225 flatten(style), 229 226 ]} 230 - contentContainerStyle={isNative ? a.pb_4xl : undefined}> 227 + contentContainerStyle={isNative ? a.pb_4xl : undefined} 228 + ref={ref}> 231 229 {children} 232 230 <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> 233 231 </BottomSheetScrollView> 234 232 ) 235 - } 233 + }) 236 234 237 235 export function Handle() { 238 236 const t = useTheme() 239 237 240 - const onTouchStart = React.useCallback(() => { 241 - Keyboard.dismiss() 242 - }, []) 243 - 244 238 return ( 245 - <View 246 - style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]} 247 - onTouchStart={onTouchStart}> 239 + <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}> 248 240 <View 249 241 style={[ 250 242 a.rounded_sm,
+1 -1
src/components/Dialog/types.ts
··· 4 4 GestureResponderEvent, 5 5 ScrollViewProps, 6 6 } from 'react-native' 7 - import {BottomSheetProps} from '@gorhom/bottom-sheet' 7 + import {BottomSheetProps} from '@discord/bottom-sheet/src' 8 8 9 9 import {ViewStyleProp} from '#/alf' 10 10
+15 -12
src/components/ReportDialog/index.tsx
··· 1 1 import React from 'react' 2 - import {View, Pressable} from 'react-native' 2 + import {Pressable, View} from 'react-native' 3 3 import {Trans} from '@lingui/macro' 4 4 5 - import {useMyLabelersQuery} from '#/state/queries/preferences' 6 5 import {ReportOption} from '#/lib/moderation/useReportOptions' 6 + import {useMyLabelersQuery} from '#/state/queries/preferences' 7 7 export {useDialogControl as useReportDialogControl} from '#/components/Dialog' 8 + 9 + import {AppBskyLabelerDefs} from '@atproto/api' 10 + import {BottomSheetScrollViewMethods} from '@discord/bottom-sheet/src' 8 11 9 12 import {atoms as a} from '#/alf' 10 - import {Loader} from '#/components/Loader' 11 13 import * as Dialog from '#/components/Dialog' 14 + import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' 15 + import {useOnKeyboardDidShow} from '#/components/hooks/useOnKeyboard' 16 + import {Loader} from '#/components/Loader' 12 17 import {Text} from '#/components/Typography' 13 - 14 - import {ReportDialogProps} from './types' 15 18 import {SelectLabelerView} from './SelectLabelerView' 16 19 import {SelectReportOptionView} from './SelectReportOptionView' 17 20 import {SubmitView} from './SubmitView' 18 - import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' 19 - import {AppBskyLabelerDefs} from '@atproto/api' 21 + import {ReportDialogProps} from './types' 20 22 21 23 export function ReportDialog(props: ReportDialogProps) { 22 24 return ( ··· 35 37 error, 36 38 } = useMyLabelersQuery() 37 39 const isLoading = useDelayedLoading(500, isLabelerLoading) 40 + 41 + const ref = React.useRef<BottomSheetScrollViewMethods>(null) 42 + useOnKeyboardDidShow(() => { 43 + ref.current?.scrollToEnd({animated: true}) 44 + }) 38 45 39 46 return ( 40 - <Dialog.ScrollableInner 41 - label="Report Dialog" 42 - keyboardDismissMode="interactive"> 47 + <Dialog.ScrollableInner label="Report Dialog" ref={ref}> 43 48 {isLoading ? ( 44 49 <View style={[a.align_center, {height: 100}]}> 45 50 <Loader size="xl" /> ··· 55 60 ) : ( 56 61 <ReportDialogLoaded labelers={labelers} {...props} /> 57 62 )} 58 - 59 - <Dialog.Close /> 60 63 </Dialog.ScrollableInner> 61 64 ) 62 65 }
+12
src/components/hooks/useOnKeyboard.ts
··· 1 + import React from 'react' 2 + import {Keyboard} from 'react-native' 3 + 4 + export function useOnKeyboardDidShow(cb: () => unknown) { 5 + React.useEffect(() => { 6 + const subscription = Keyboard.addListener('keyboardDidShow', cb) 7 + 8 + return () => { 9 + subscription.remove() 10 + } 11 + }, [cb]) 12 + }
+20 -20
src/view/com/modals/Modal.tsx
··· 1 - import React, {useRef, useEffect} from 'react' 1 + import React, {useEffect, useRef} from 'react' 2 2 import {StyleSheet} from 'react-native' 3 3 import {SafeAreaView} from 'react-native-safe-area-context' 4 - import BottomSheet from '@gorhom/bottom-sheet' 5 - import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' 6 - import {usePalette} from 'lib/hooks/usePalette' 4 + import BottomSheet from '@discord/bottom-sheet/src' 7 5 8 - import {useModals, useModalControls} from '#/state/modals' 9 - import * as EditProfileModal from './EditProfile' 10 - import * as RepostModal from './Repost' 11 - import * as SelfLabelModal from './SelfLabel' 12 - import * as ThreadgateModal from './Threadgate' 13 - import * as CreateOrEditListModal from './CreateOrEditList' 14 - import * as UserAddRemoveListsModal from './UserAddRemoveLists' 15 - import * as ListAddUserModal from './ListAddRemoveUsers' 6 + import {useModalControls, useModals} from '#/state/modals' 7 + import {usePalette} from 'lib/hooks/usePalette' 8 + import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' 9 + import * as AddAppPassword from './AddAppPasswords' 16 10 import * as AltImageModal from './AltImage' 17 11 import * as EditImageModal from './AltImage' 18 - import * as DeleteAccountModal from './DeleteAccount' 12 + import * as ChangeEmailModal from './ChangeEmail' 19 13 import * as ChangeHandleModal from './ChangeHandle' 14 + import * as ChangePasswordModal from './ChangePassword' 15 + import * as CreateOrEditListModal from './CreateOrEditList' 16 + import * as DeleteAccountModal from './DeleteAccount' 17 + import * as EditProfileModal from './EditProfile' 18 + import * as EmbedConsentModal from './EmbedConsent' 19 + import * as InAppBrowserConsentModal from './InAppBrowserConsent' 20 20 import * as InviteCodesModal from './InviteCodes' 21 - import * as AddAppPassword from './AddAppPasswords' 22 21 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' 23 22 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' 24 - import * as VerifyEmailModal from './VerifyEmail' 25 - import * as ChangeEmailModal from './ChangeEmail' 26 - import * as ChangePasswordModal from './ChangePassword' 27 - import * as SwitchAccountModal from './SwitchAccount' 28 23 import * as LinkWarningModal from './LinkWarning' 29 - import * as EmbedConsentModal from './EmbedConsent' 30 - import * as InAppBrowserConsentModal from './InAppBrowserConsent' 24 + import * as ListAddUserModal from './ListAddRemoveUsers' 25 + import * as RepostModal from './Repost' 26 + import * as SelfLabelModal from './SelfLabel' 27 + import * as SwitchAccountModal from './SwitchAccount' 28 + import * as ThreadgateModal from './Threadgate' 29 + import * as UserAddRemoveListsModal from './UserAddRemoveLists' 30 + import * as VerifyEmailModal from './VerifyEmail' 31 31 32 32 const DEFAULT_SNAPPOINTS = ['90%'] 33 33 const HANDLE_HEIGHT = 24
+13 -12
src/view/com/modals/SwitchAccount.tsx
··· 5 5 TouchableOpacity, 6 6 View, 7 7 } from 'react-native' 8 - import {Text} from '../util/text/Text' 9 - import {s} from 'lib/styles' 10 - import {usePalette} from 'lib/hooks/usePalette' 8 + import {BottomSheetScrollView} from '@discord/bottom-sheet/src' 9 + import {msg, Trans} from '@lingui/macro' 10 + import {useLingui} from '@lingui/react' 11 + 12 + import {useProfileQuery} from '#/state/queries/profile' 13 + import {SessionAccount, useSession, useSessionApi} from '#/state/session' 14 + import {useCloseAllActiveElements} from '#/state/util' 11 15 import {useAnalytics} from 'lib/analytics/analytics' 16 + import {Haptics} from 'lib/haptics' 12 17 import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher' 13 - import {UserAvatar} from '../util/UserAvatar' 18 + import {usePalette} from 'lib/hooks/usePalette' 19 + import {makeProfileLink} from 'lib/routes/links' 20 + import {s} from 'lib/styles' 14 21 import {AccountDropdownBtn} from '../util/AccountDropdownBtn' 15 22 import {Link} from '../util/Link' 16 - import {makeProfileLink} from 'lib/routes/links' 17 - import {BottomSheetScrollView} from '@gorhom/bottom-sheet' 18 - import {Haptics} from 'lib/haptics' 19 - import {Trans, msg} from '@lingui/macro' 20 - import {useLingui} from '@lingui/react' 21 - import {useSession, useSessionApi, SessionAccount} from '#/state/session' 22 - import {useProfileQuery} from '#/state/queries/profile' 23 - import {useCloseAllActiveElements} from '#/state/util' 23 + import {Text} from '../util/text/Text' 24 + import {UserAvatar} from '../util/UserAvatar' 24 25 25 26 export const snapPoints = ['40%', '90%'] 26 27
+1 -1
src/view/com/modals/util.tsx
··· 1 1 export { 2 2 BottomSheetScrollView as ScrollView, 3 3 BottomSheetTextInput as TextInput, 4 - } from '@gorhom/bottom-sheet' 4 + } from '@discord/bottom-sheet/src'
+1 -1
src/view/com/util/BottomSheetCustomBackdrop.tsx
··· 1 1 import React, {useMemo} from 'react' 2 2 import {TouchableWithoutFeedback} from 'react-native' 3 - import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet' 4 3 import Animated, { 5 4 Extrapolate, 6 5 interpolate, 7 6 useAnimatedStyle, 8 7 } from 'react-native-reanimated' 8 + import {BottomSheetBackdropProps} from '@discord/bottom-sheet/src' 9 9 import {msg} from '@lingui/macro' 10 10 import {useLingui} from '@lingui/react' 11 11
+12 -8
yarn.lock
··· 2814 2814 pino "^8.11.0" 2815 2815 pino-http "^8.3.3" 2816 2816 2817 + "@discord/bottom-sheet@https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1": 2818 + version "4.6.1" 2819 + resolved "https://github.com/bluesky-social/react-native-bottom-sheet.git#54dc2e0e318b0524a2d2d8fb817f6c48101bb0b1" 2820 + dependencies: 2821 + "@gorhom/portal" "1.0.14" 2822 + invariant "^2.2.4" 2823 + 2817 2824 "@discoveryjs/json-ext@^0.5.0": 2818 2825 version "0.5.7" 2819 2826 resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" ··· 3601 3608 version "1.1.3" 3602 3609 resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" 3603 3610 integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== 3604 - 3605 - "@gorhom/bottom-sheet@^4.5.1": 3606 - version "4.5.1" 3607 - resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-4.5.1.tgz#1ac4b234a80e7dff263f0b7ac207f92e41562849" 3608 - integrity sha512-4Qy6hzvN32fXu2hDxDXOIS0IBGBT6huST7J7+K1V5bXemZ08KIx5ZffyLgwhCUl+CnyeG2KG6tqk6iYLkIwi7Q== 3609 - dependencies: 3610 - "@gorhom/portal" "1.0.14" 3611 - invariant "^2.2.4" 3612 3611 3613 3612 "@gorhom/portal@1.0.14": 3614 3613 version "1.0.14" ··· 7628 7627 integrity sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA== 7629 7628 dependencies: 7630 7629 "@types/node" "*" 7630 + 7631 + "@types/invariant@^2.2.37": 7632 + version "2.2.37" 7633 + resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.37.tgz#1709741e534364d653c87dff22fc76fa94aa7bc0" 7634 + integrity sha512-IwpIMieE55oGWiXkQPSBY1nw1nFs6bsKXTFskNY8sdS17K24vyEBRQZEwlRS7ZmXCWnJcQtbxWzly+cODWGs2A== 7631 7635 7632 7636 "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": 7633 7637 version "2.0.4"